Developers › Ingestion API
Developer Reference Ingest a session The ingestion path end to end, from minting a key to reading a processed lead's status. Where your AI Agents already write to LeadLogiX, steps 2 and 5 are already done and only 1, 3 and 4 are yours to wire. Mint an ingestion key In Settings → API Playground , a workspace admin generates a key ( val_key__… ). Copy it once and store it securely. This is the credential your AI Agents present when they notify LeadLogiX. Your AI Agent finishes a conversation The Agent writes the transcript and context to LeadLogiX's raw lead storage, keyed by a sessionId you choose. If you use Agents supplied with LeadLogiX this is already done. If you are building your own Agent, this step is yours — and it is the one that decides whether the session is scored at all. The record has a contract, and it is fully specified in Session object : the metadata contract ( sessionId , content , started and embedding are mandatory — their absence rejects the session outright), the transcript grammar your content must follow, the container tags the document must carry (your workspace tag first), and the minimum fingerprint . Read those four before writing the emitter; between them they decide whether a session is scored, silently degraded, or never opened at all. The transport that writes the record is not self-service yet — see What you can build today . Notify LeadLogiX the session is ready POST the sessionId and a processor mode — the line of business this conversation belongs to ( 2 real estate, 3 US life insurance; both on every plan). LeadLogiX queues it and returns 202 ACCEPTED immediately. Re-notifying the same sessionId is safe — it never re-queues. The same call in curl, Node and Python is on the Quickstart . POST /api/v1/sessions Content-Type: application/json X-API-Key: val_key__… { "sessionId": "conv_9F3A21", "processor": 2 } # → 202 Accepted { "status": "ACCEPTED", "sessionId": "conv_9F3A21", "processing": { "state": "QUEUED" } } Notify as soon as the conversation ends — don't wait Write your final turn, then notify. LeadLogiX waits for the record itself and retries while it is still assembling, so you do not need to poll storage first. In particular do not wait for the written summary : it is produced by a separate enrichment pass that runs afterwards and is batched — measured on a live session, the transcript was complete in about 10 seconds and the summary still had not appeared three minutes later. A lead whose summary has not landed yet still processes; it is simply noted and revisited. Waiting for it only delays the lead. Mint ids that stay distinct once punctuation and case are removed — chat-1 and chat_1 are one session, and the second is never scored. The rule . Poll for status until it's terminal Poll every couple of seconds until processing.state is PROCESSED , DEGRADED , or FAILED . The status is safe to expose — it carries conformance and progress, never PII, score, or price. GET /api/v1/sessions/conv_9F3A21/status X-API-Key: val_key__… # → 200 OK { "sessionId": "conv_9F3A21", "ingestedAt": "2026-07-25T14:03:11.000Z", "processorMode": 2, "processorLabel": "REALESTATE", "conformance": { "level": "ACCEPTED_WITH_NOTES", "evaluatedAt": "2026-07-25T14:03:20.000Z", "notes": [ { "code": "SUMMARY_MISSING", "path": "summary", "message": "…" } ] }, "processing": { "state": "PROCESSED", "reprocessingCandidate": false } } Notes are not failures: ACCEPTED_WITH_NOTES is the everyday result, because the level is simply "were there any notes". Branch on processing.state ; read the notes to improve the payload. Every field of this document is on Session object . A session waiting for its metadata is not a rejected one — do not give up on it A document whose content has landed but whose metadata has not is re-read on a backoff rather than rejected, so a session can sit at QUEUED with waitingForSource: true for roughly 200 seconds while processing.awaitingMandatoryKeys names the keys still missing. That is a diagnosis, not a verdict: the keys usually arrive, and a client that gives up abandons a session about to succeed. Both waiting fields and rejection contain missingMandatoryKeys — branch on the field name, never on that shape. Field Tense Read it as processing.awaitingMandatoryKeys Now The keys this session is still waiting for. Present only while it waits. processing.lastAwaitingMandatoryKeys Past The same thing, kept after the session settles, with an observedAt — so it appears on PROCESSED sessions too. Both present: still waiting. Only this one: the wait is over and processing.state says how it ended. Watch it on sessions that succeed : a connector beginning to lose leads shows up here first, as sessions that waited and recovered — your writer is sending metadata later than content, one slow index away from a rejection you cannot appeal. Work the scored lead The lead now lives in your Clearing Pool — scored, tagged and priced. From here it flows to your CRM and chat tools automatically; see Webhooks & polling .
Choosing a processor mode One field, and it decides which pipeline runs. processor is not a formality, and the four values are not degrees of one thing. Two of them are lines of business and two are pipeline depths. 2 and 3 each run the whole product against their own calibration; 1 and 0 run part of it. Choosing between 2 and 3 is a question about what the conversation was about; choosing between 2 and 1 is a question about how much of LeadLogiX you want to run. Mode What runs What you get back What you give up 2 REALESTATE All plans The full pipeline: identity, geocoding, vetting, quality scoring, pricing, tagging. A scored, priced, tagged lead in your Clearing Pool, exportable to a CRM. Nothing. A full line of business, available on every plan. 3 LIFE_INSURANCE_US All plans The same full pipeline, against the US life-insurance calibration and its own fact set: product type, coverage amount, term length, reason for cover, residence state, beneficiary named, ready to apply. A scored, priced, tagged life lead — compared against life leads rather than property leads, and priced by life calibration. Subject geocoding. A policy has no address, so no subject location is looked up and none is claimed. The residence of the person is still resolved: on this line of business it is the licensing gate. 1 IDENTITY_ONLY Unlimited Identity resolution, then stop. A match verdict — is this the person from session X? — and profile stitching. No score, no price, no geocoding, no CRM-ready lead. Nothing enters the paywall. 0 AUTOMATION_ONLY Unlimited Your automations only — the scoring engine is skipped entirely. Whatever your automations do (webhooks, alerts, CRM writes you drive yourself). No identity, no score, no price. LeadLogiX is acting as a trigger, not a scorer. Mode 3 is not a flavour of mode 2 Four differences an integration will notice, all of them deliberate. 1. Money means something else. budget on a mode-3 lead is a monthly premium ; on mode 2 it is a one-off purchase price. The two are orders of magnitude apart — a large policy costs tens of dollars a month — so never sum, average or compare them, and label each from its own line of business rather than printing a bare "Budget". The coverage amount discussed is a separate fact and is not the budget. 2. The roles are disjoint. OWNER , INSURED , BENEFICIARY and PAYER — four roles that can all sit on one contract and are not synonyms — where mode 2 has BUYER and SELLER . Map an unfamiliar role to unknown, not onto the nearest role you already handle. 3. Nothing about the subject is geocoded. The lead's residence is resolved as usual, and on this line of business it matters more than anywhere else — it decides which licensed agents may lawfully contact the person. But a policy itself has no location, so no subject lookup is performed and no proximity result is claimed. Each line of business also declares the markets it serves in its own calibration ( supportedGeocodingCountries ); LIFE_INSURANCE_US serves the United States, so a residence outside it degrades honestly to stateCode: "XX" rather than being re-aimed at a market that cannot contain the answer. 4. Health details are never kept. A life conversation contains them — it is how callers expect to be quoted — and LeadLogiX records none of it, scores none of it and releases none of it. That is a rule, not a gap: an empty health field is not evidence the caller stayed silent, so do not read one as a finding. The plan gate never blocks a line of business Both 2 and 3 work on every plan. Only modes 0 and 1 are refused on a plan that does not carry them, with 403 PLAN_UPGRADE_REQUIRED — named, never silently downgraded, so you find out at integration time instead of discovering months of unscored leads. The mode is per session, not per workspace Nothing stops you sending mode 1 for one conversation and mode 2 for the next. They are different sessions with different ids.
What each mode returns The status document, mode by mode — and where the actual result comes out. The notify API never returns a result. It returns progress — the diagram . Whatever the mode, a successful poll looks the same shape; what differs is processorLabel , whether conformance is populated, and where you go to collect the outcome. Mode Terminal status you poll conformance Where the result actually is 2 REALESTATE { state: "PROCESSED" } , processorLabel: "REALESTATE" Populated — CONFORMANT or ACCEPTED_WITH_NOTES The scored lead. Out via CRM sync, alerts/automations, the dashboard, or MCP search_leads / get_lead . Not in this response. 3 LIFE_INSURANCE_US { state: "PROCESSED" } , processorLabel: "LIFE_INSURANCE_US" Populated, graded identically to mode 2 — CONFORMANT or ACCEPTED_WITH_NOTES . A full line of business also rejects a source document missing a mandatory metadata key, exactly as mode 2 does; modes 0 and 1 never do. The scored lead, in the same places. Its facts are life facts — product type, coverage amount, term length, residence state — and its budget is a monthly premium. 1 IDENTITY_ONLY { state: "PROCESSED" } , processorLabel: "IDENTITY_ONLY" Populated, and graded like mode 2 — CONFORMANT or ACCEPTED_WITH_NOTES . Mode 1 opens the source document, so the contract can be checked against it. A resolved userId and an identity verdict written onto the lead, feeding Customer Profiles. Not readable through this API — see below. 0 AUTOMATION_ONLY { state: "PROCESSED" } , processorLabel: "AUTOMATION_ONLY" Terminal { level: "NOT_EVALUATED", reason: "AUTOMATION_ONLY_NO_DOCUMENT_FETCH" } — the source is never fetched, so there is nothing to grade Whatever your automations did — a webhook you received, a CRM row you wrote, an alert you were sent. Never poll for conformance — poll for processing.state processing.state is the completion signal. Conformance is an observation about the payload, and on mode 0 there is no payload to observe: the level settles on the terminal value NOT_EVALUATED , with reason naming why and evaluatedAt present and null . Polling will never move it, and it is not a fault — read reason and stop. Only PENDING is worth waiting on, and it now means what it says: still in flight. An integration that waits for CONFORMANT will still wait for ever on a mode-0 session that finished perfectly, because CONFORMANT is a grade and mode 0 never earns one. Mode 0 completes with lead.automation_ready The engine never runs, so no lead.processed is emitted — that would imply a pipeline ran, and would change what every existing rule fires on, since a rule with no lines named runs on all of them. Automation rules that react to a mode-0 session completing subscribe to lead.automation_ready . lead.ingested is arrival, not completion Every mode emits lead.ingested , and it fires while the session is still being accepted — the lead reads QUEUED at that moment, with no processing result on it yet. Subscribe to it when you want to know a session ARRIVED. To act on what a session produced, use the completion event for its line: lead.automation_ready for mode 0, lead.identity_resolved for mode 1, lead.processed for a full vertical. Metadata filters and mode 0 A mode-0 rule cannot use a metadata filter. The metadata lives in the source document, which mode 0 is defined never to fetch, so it never reaches the lead at all. Target a mode-0 rule by its line and workspace, or use a document-watch rule, which filters on the lead document itself. Mode 1 has no such limitation. If your agent needs an identity answer DURING a conversation, mode 1 is the wrong tool Mode 1 is asynchronous and its verdict is not in the status response — it exists to stitch profiles in the background. To ask "is this the person from session X?" and branch on the answer while the visitor is still talking, call POST /api/v1/identity/lookup — the Recognise a returning customer recipe — or POST /api/v1|v2/identity/resolve when you already hold the candidates. Both answer synchronously with a verdict. Reaching for mode 1 and then polling for something that never arrives is the most common wrong turn on this API.
Reading results back Two credentials, two surfaces — and an ingestion key cannot read. An ingestion key ( val_key__… ) writes and asks; it does not browse. It can notify and poll sessions, run the identity calls, and submit and poll a profile merge — but no endpoint lists or searches scored leads for a key. The only lead a key can read is one an identity lookup pointed it at, through GET /api/v1/leads/{leadId}/context under the same paywall and scrub as everywhere else. That is deliberate: a key lives in an Agent runtime, and scored leads carry contact details and price. You want Use Credential To submit a finished conversation for scoring POST /api/v1/sessions Ingestion key ( X-API-Key ) To know whether it finished GET /api/v1/sessions/:id/status Ingestion key A same-person verdict, synchronously POST /api/v1|v2/identity/resolve Ingestion key To ask mid-conversation whether this workspace already knows the caller POST /api/v1/identity/lookup Ingestion key To follow a pointer that lookup returned GET /api/v1/leads/{leadId}/context , GET /api/v1/profiles/{profileId} Ingestion key To READ scored leads back into an agent MCP — search_leads , get_lead , get_lead_brief , list_lead_tags OAuth 2.1 access token with leads:read To ask whether a lead reached the CRM, or who can be given it MCP — get_lead_crm_status , list_crm_agents OAuth 2.1 access token with leads:read To read one customer’s whole history back into an agent MCP — get_customer_profile OAuth 2.1 access token with profiles:read Leads pushed to you without polling CRM sync, a custom webhook job, or an alert automation Configured in the workspace, not by API GET /api/leads is not part of the API It exists, and it will refuse your key. It is the dashboard's own endpoint and requires an admin login session , not an ingestion key. If you are building something that needs to read leads programmatically, that is MCP — see MCP → Hosted server , or the two read-back endpoints in Identity & Profiles API for a lead the identity lookup pointed you at.
Verifying your integration Four things worth proving before you go live. Prove How The happy path Send a complete session in the Playground with Sandbox on , then poll until processing.state is PROCESSED . That is the pass condition. Read conformance.notes and fix what they name, but do not assert on CONFORMANT — see below. Your rejection handling Deliberately omit started from the conversation record and notify. You should get a terminal FAILED carrying rejection.missingMandatoryKeys: ["started"] — assert on the code , not the message. Your note handling Omit an expected key such as speakers . The session still reaches PROCESSED , with conformance.level: ACCEPTED_WITH_NOTES . If your integration treats that as a failure, it is wrong — notes are advisory. Your retry logic Notify the same sessionId twice. The second call must return 202 with alreadySubmitted: true and must not create a second lead. Do not assert on CONFORMANT — a correct integration usually is not CONFORMANT means zero notes of any kind, and two routine notes are not yours to remove. SUMMARY_MISSING appears whenever the written summary has not landed by the time the pipeline runs — a separate, batched pass, measured still absent three minutes after a transcript was complete — and SPEC_VERSION_MISSING appears on any connector that omits leadvalSpecVersion . A build gated on CONFORMANT goes red for timing rather than for a defect. Gate on processing.state === "PROCESSED" , and review the note code s as a quality report rather than a gate. Do not test by re-sending a rejected session id A sessionId is spent the moment it is accepted, rejected included. Re-notifying returns the stored outcome rather than reprocessing, so a fix you make after a rejection needs a new id to be observable. Remember the id is compared with punctuation removed and case folded — changing conv-1 to conv_1 is not a new id.
Knowing which of your users exist in the CRM Checked daily by LeadLogiX; read it through the hosted MCP server. A workspace can connect several CRMs at once. Once a day LeadLogiX reads the people in each one — Salesforce User , HubSpot owners — and matches them against your workspace members by normalised email . There is no REST endpoint for the result. An application reads it with the hosted MCP tool list_crm_agents : each member comes back with a crm entry per connected provider carrying its state , canBeAssignedWork and checkedAt , plus assignableInAnyCrm across all of them. With the workspace:read.contact scope each entry also carries the crmUserId to assign work to and, when the match is ambiguous, candidateIds . Admins see the same states as badges under Workspaces & Users . state Meaning Safe to route work to? ACTIVE Matched; the seat is live and can own records. Yes INACTIVE Matched, but deactivated or archived in the CRM. No NOT_OWNABLE Matched and active, but not a staff seat — an integration user, an automated process, or a queue. No AMBIGUOUS Several CRM users share this email. candidateIds lists them; LeadLogiX refuses to pick. No NOT_FOUND No CRM user has this email. No UNKNOWN The check did not complete, or the CRM has more users than LeadLogiX stores. Treat as unknown, not absent Never collapse these to a boolean The three states that are neither ACTIVE nor NOT_FOUND each need a different response, and UNKNOWN in particular is not a statement about the person — it means the last refresh failed, the connection lacks the scope, or the CRM has more users than fit in one stored roster. Code that reads state === "ACTIVE" ? "in CRM" : "not in CRM" will confidently report real staff as missing during an outage. Why matching is exact-email only, and why an outage never empties the list No name fuzzing. Real CRM data contains duplicate addresses — one live org returned the same email on three separate user records — so a duplicate produces AMBIGUOUS rather than a guess. Assigning a lead to the wrong colleague is worse than assigning it to nobody. And a failed refresh never overwrites a good roster: LeadLogiX keeps the previous list and records the failure, so a CRM outage cannot read as "everyone left". Prefer to click than to curl? The API Playground runs all of the above against the real endpoints in sandbox mode, and hands you a ready-made cURL command for each request.