Leads guide

A lead is a person Lojiq may call or text on your behalf. This guide covers the fields that matter (especially consent), how deduplication works, the bulk CSV path, and what you get back.

The lead object#

phone_number string, E.164 required
The dedupe key. Any format with 10–15 digits is accepted and stored as E.164 (5551234567 becomes +15551234567); send E.164 so what you store matches. It cannot be changed afterwards.
first_name, last_name string
Used by agents and texting journeys ({{first_name}} in prompts).
email_address string
Stored; not used for outreach by Lojiq today.
status string
Starts as new. After dialing, Lojiq writes the last outcome here (contacted, no_answer, busy, voicemail, do_not_call, …). Treat it as an open set; a PATCH that changes it emits lead.disposition_changed.
campaign_id uuid
Attach the lead to a campaign you own; it becomes dialable when that campaign runs. Omit to keep it in the lead bank.
consent_source string
Where consent was collected. Free text; use something you can defend later: web_form, signed_application, inbound_call, in_store_signup. Defaults to public_api (or csv_import on bulk rows).
consented_at date-time
When the person agreed to be contacted. Send it whenever you have it.
credit_score, annual_income number
Credit score 300–850, income above 0. Stored for qualification prompts; also available as CSV columns.
notes string
Up to 4,000 characters of free text for your own use.

Those are the only fields a write accepts; any other key answers 400 unknown_field:<name>. Reads return the stored record, which also contains full_name (derived from the names, or Lead 4567 from the last four digits), source_tag (api:standalone or api:campaign:<id> for leads you create here) and internal columns Lojiq uses for dialing and texting (last_call_outcome, global_dnc, is_suppressed, …). The internal columns are not part of the versioned contract; do not build on them.

You are responsible for consent

Lojiq places AI-voice and autodialed calls and sends automated texts. In the US that outreach requires the recipient's prior express written consent for marketing, and the Do-Not-Call rules apply regardless. Lojiq enforces its own protections (your organization's DNC list, opt-out detection in calls and texts, calling-hour windows, per-day frequency caps) but it cannot know whether your form really collected consent. Send consent_source and consented_at with every lead so there is a record, keep the original consent artifact on your side, and never import purchased lists without consent that names you.

Creating one lead#

bash
curl -s -X POST "https://api.lojiq.ai/v1/leads" \
  -H "Authorization: Bearer $LOJIQ_KEY" -H "Content-Type: application/json" \
  -d '{"phone_number":"+15551234567","first_name":"Avery","last_name":"Rivera",
       "campaign_id":"0d2f6b1e-4a7c-4e8d-9b1a-2c3d4e5f6a7b",
       "consent_source":"web_form","consented_at":"2026-10-02T16:58:00Z"}'

201 means inserted; 200 means a lead with that phone number already existed in your organization and was updated with the fields you sent (its id is unchanged). Both return the full record. A campaign_id that is not one of your campaigns answers 404 campaign_not_found. Reference →

Send an Idempotency-Key header (any unique string, a UUID is ideal) and a retried request returns the first answer with Idempotency-Replayed: true instead of running again. Retry safety →

Deduplication rules#

  • Scope: your organization only. The same phone number in two Lojiq organizations is two leads.
  • Key: the phone number in canonical E.164 form. +15551234567, 5551234567 and (555) 123-4567 all match the same lead; a 10-digit number gets +1. The bulk importer uses the same rule.
  • On a match, the fields you send overwrite the stored ones; fields you omit are kept — except status and consent_source, which take their defaults (new, public_api) on every create call. To change one field of a lead that is already being worked, look it up with GET /leads?phone_number=… and PATCH it instead; and always send consent_source on create so a repeat does not overwrite your consent record.
  • Dedupe emits lead.updated; insert emits lead.created. Both carry deduped and source: "public_api".

Updating a lead#

bash
curl -s -X PATCH "https://api.lojiq.ai/v1/leads/4b1e0d1a-3c4e-4d0a-9f2b-7e1a2b3c4d5e" \
  -H "Authorization: Bearer $LOJIQ_KEY" -H "Content-Type: application/json" \
  -d '{"status":"do_not_call"}'

Send only what changes. Changing status emits lead.disposition_changed; anything else emits lead.updated; both carry changes, the list of field names you sent. Unknown fields answer 400 unknown_field:<name>, phone_number answers 400 phone_number_immutable, an empty body 400 nothing_to_update, and a lead that is not in your organization 404 not_found. Setting status to do_not_call records your wish on the lead; to stop all outreach to a number, also add it to your organization's DNC list in the app.

Bulk import from CSV#

One request, thousands of rows, synchronous. Three steps.

  1. Preflight#

    POST /leads/inspect-csv parses the file and maps your headers onto Lojiq's columns using an alias list (Phone, Mobile, Cell → phone_number; Email → email_address; FICO → credit_score; opt_in_date → consented_at; …). Nothing is stored.

    bash
    curl -s -X POST "https://api.lojiq.ai/v1/leads/inspect-csv" \
      -H "Authorization: Bearer $LOJIQ_KEY" -H "Content-Type: application/json" \
      -d "$(jq -n --rawfile csv ./leads.csv '{csv:$csv}')"
    
    200 OK
    {
      "total_rows": 1240,
      "headers": ["First", "Last", "Phone", "Email", "Opt-in date"],
      "suggested_mapping": { "first_name": "First", "last_name": "Last", "phone_number": "Phone", "email_address": "Email" },
      "unmapped": ["Opt-in date"],
      "missing_required": [],
      "sample_rows": [ { "First": "Avery", "Last": "Rivera", "Phone": "(555) 123-4567", "Email": "avery@example.com", "Opt-in date": "2026-09-30" } ]
    }
    

    missing_required non-empty means no column maps to phone_number: fix the file or supply the mapping by hand. Headers in unmapped are ignored unless you map them yourself (here, add "consented_at": "Opt-in date").

  2. Import#

    bash
    curl -s -X POST "https://api.lojiq.ai/v1/leads/bulk" \
      -H "Authorization: Bearer $LOJIQ_KEY" -H "Content-Type: application/json" \
      -d "$(jq -n --rawfile csv ./leads.csv \
            '{csv:$csv, campaign_id:"0d2f6b1e-4a7c-4e8d-9b1a-2c3d4e5f6a7b",
              mapping:{first_name:"First", last_name:"Last", phone_number:"Phone", email_address:"Email", consented_at:"Opt-in date"}}')"
    
    202 Accepted
    { "job_id": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d0e", "total": 1240, "inserted": 1201, "updated": 37, "skipped": 0, "failed": 2 }
    

    Without mapping, the headers must already be the canonical names (first_name, last_name, phone_number, email_address, status, credit_score, annual_income, consent_source, consented_at); the alias list is not applied on import, only on inspect. With campaign_id, every row lands in that campaign.

  3. Check the counts#

    failed rows had no usable phone number or a missing required column. Each failure is stored against the job with its row number and raw row; an endpoint to read them is on the roadmap — for now, keep the file and the job_id, and ask support if you need the detail. The import emits one lead.created / lead.updated per row (with source: "csv_import" and the job_id), so a 10,000-row file produces 10,000 deliveries if you subscribe to those events.

Size guidance

The import runs inside the request. Keep each request under about 5,000 rows and under 5 MB of CSV (413 payload_too_large above 5,000,000 bytes) and split larger files; a timeout mid-file leaves the rows already processed in place, so re-sending the whole file is safe but noisy (every processed row becomes an updated row and emits lead.updated again).

Export#

bash
curl -s "https://api.lojiq.ai/v1/leads.csv?campaign_id=0d2f6b1e-4a7c-4e8d-9b1a-2c3d4e5f6a7b" \
  -H "Authorization: Bearer $LOJIQ_KEY" -o lead-bank.csv

Streams first_name, last_name, phone_number, email_address, status, credit_score, annual_income, consent_source, consented_at, created_at, lead_id, oldest first. Leave out campaign_id for the whole lead bank.

Listing and filtering#

bash
# the 200 most recent leads in one campaign
curl -s "https://api.lojiq.ai/v1/leads?campaign_id=0d2f…&limit=200&offset=0" -H "Authorization: Bearer $LOJIQ_KEY"
# one lead by phone (any format; canonicalised before matching)
curl -s "https://api.lojiq.ai/v1/leads?phone_number=%2B15551234567" -H "Authorization: Bearer $LOJIQ_KEY"
# every lead you marked do_not_call
curl -s "https://api.lojiq.ai/v1/leads?status=do_not_call" -H "Authorization: Bearer $LOJIQ_KEY"

Lists are newest first and return a summary (id, first_name, last_name, full_name, phone_number, email_address, status, campaign_id, consent_source, consented_at, created_at, updated_at); GET /leads/{id} returns everything. There is no total count: page until data is shorter than limit.

Events leads emit#

EventWhendata
lead.createdPOST /leads inserted, or a bulk row inserted{ lead_id, deduped: false, source: "public_api" } / { lead_id, source: "csv_import", job_id }
lead.updatedDeduplicated create, bulk row updated, or a PATCH without status{ lead_id, deduped: true, source: "public_api" } / { lead_id, source, job_id } / { lead_id, changes: [field names], source: "public_api" }
lead.disposition_changedPATCH /leads/{id} with status{ lead_id, changes: [field names], source: "public_api" }

Dispositions written by the dialer and by reps in the app do not emit lead.disposition_changed yet; the per-call outcome reaches you through call.transcript_ready (and the lead's status on the next read). See the Webhooks guide for what fires today.

Patterns#

  • Web form → Lojiq: POST /leads on submit with consent_source: "web_form" and consented_at: now. Retries are safe.
  • CRM sync, both ways: push with POST /leads; pull outcomes with call.transcript_ready and appointment.booked, matching on lead_id (store it on your side when you create the lead).
  • Nightly list load: inspect-csv once to settle the mapping, then POST /leads/bulk in ≤5,000-row chunks into the campaign for the day.
  • Suppression: when someone opts out on your side, PATCH the lead to do_not_call and add the number to the DNC list in the app.