This guide is for lead vendors, comparison sites and your own systems. It covers sending people to your Subcontext organisation as leads and finding out what became of them. Send leads from your server covers the first request in more depth, with examples in several languages.
You need a server-only application with the read, write and crm
scopes (see Access tokens). Each application only ever
sees the leads it sent itself, so give each partner or system its own.
Send a lead#
curl -X POST https://subcontext.com/api/v1/crm/ingests \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: web-12345" \
-d '{
"source": "my_website",
"campaign": "spring-life-2026",
"identity": { "first_name": "Ada", "last_name": "Lovelace", "email": "ada@example.com", "phone": "07700 900123" },
"data": { "cover_wanted": 250000, "reason": "New mortgage" },
"consent": { "service": { "email": true, "phone": true }, "method": "web_form" },
"provenance": { "page_url": "https://www.example.com/life?utm_source=newsletter&utm_campaign=spring" },
"source_reference": "web-12345"
}'
| Field | What it is |
|---|---|
source |
Required. Your system's name. Lower-case letters, digits, _, . and -, up to 64 characters. |
campaign |
The campaign's key, from CRM, Campaigns in the dashboard. The campaign's rules decide which leads it accepts. |
identity |
Who the person is: any of email, phone, name, first_name, last_name, and external_id (their id in your system). |
data |
Anything else you know, in any shape. Subcontext reads it. |
declared |
Values to store exactly as given, never interpreted (plain values, not nested). |
text |
Free text, such as a message the person wrote. |
consent |
What the person agreed to, as your form recorded it. |
provenance |
Where they came from, as page_url (with its utm_ tags) and referrer. |
occurred_at |
When it happened, if not now. |
source_reference |
Your own reference, shown back to you. |
You need at least one of identity, data, declared or text. A lead can
be up to 256 KB.
The answer is 202 Accepted:
{
"data": {
"id": "0199a3c2-8100-7411-9d92-162738a9bacb",
"status": "received",
"source": "my_website",
"source_reference": "web-12345",
"received_at": "2026-10-12T10:40:12+01:00",
"facts_written": 0,
"fields_proposed": 0,
"activities_written": 0,
"truncated": false,
"provenance": {
"v": 1, "surface": "api", "page_url": "https://www.example.com/life?utm_source=newsletter&utm_campaign=spring",
"host": "www.example.com", "path": "/life", "utm": { "source": "newsletter", "campaign": "spring" },
"in_iframe": false, "channel": "my_website", "captured_at": "2026-10-12T09:40:12Z"
},
"explanation": "Accepted; working out who this is about.",
"duplicate": false
}
}
202 means Subcontext has stored the lead. Working out who it's about and
reading it happens in the background, in a few seconds. The Location header
has the address to check on it.
Always send an Idempotency-Key, ideally your own record's id. If a request
times out, send it again with the same key. Within 10 minutes you get the
first answer back (with an Idempotent-Replay: true header). After that,
duplicate is true and you get the original lead. Neither makes a second
lead.
If the lead is refused, 409 campaign_paused or campaign_archived means the
campaign isn't taking leads right now (the lead may be accepted later). 422
means it can never be accepted as sent (unknown_campaign, or
validation_failed with a message saying what to fix, and fields naming
the input when it's one).
Check it was taken in#
Poll the ingest until it's finished:
curl https://subcontext.com/api/v1/crm/ingests/0199a3c2-8100-7411-9d92-162738a9bacb \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": {
"id": "0199a3c2-8100-7411-9d92-162738a9bacb",
"status": "applied",
"source": "my_website",
"source_reference": "web-12345",
"received_at": "2026-10-12T10:40:12+01:00",
"processed_at": "2026-10-12T10:40:15+01:00",
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
"facts_written": 3,
"fields_proposed": 0,
"activities_written": 0,
"truncated": false,
"explanation": "Applied to contact 0199a3c2-8200-7522-8ea3-2738a9bacbdc; 3 fact(s) recorded."
}
}
status |
What it means |
|---|---|
received, resolving, extracting |
Still being worked on. Check again in a few seconds. |
applied |
Done. contact_id is the lead's id. Keep it. |
held |
It didn't meet the campaign's rules. validation_failures says which, in your field names. Your firm's team can still accept it. |
needs_review |
The details could belong to more than one person, or there was nothing to identify anyone. A person at your firm decides. |
deferred |
Paused, for example by a daily limit. It carries on by itself. |
failed, discarded |
It won't be processed. error says why. |
explanation says the same in words, for your logs.
See where a lead is now#
With the contact_id:
curl https://subcontext.com/api/v1/crm/leads/0199a3c2-8200-7522-8ea3-2738a9bacbdc \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": {
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
"external_system_id": "CRM-88123",
"status": "won",
"terminal": true,
"stage": { "key": "won", "name": "Won", "kind": "won", "probability": 100 },
"stage_entered_at": "2026-10-14T16:02:11+01:00",
"owner_assigned": true,
"score": 71,
"campaign_id": "0199a3c2-8300-7633-9fb4-38a9bacbdced",
"first_touched_at": "2026-10-12T10:52:40+01:00",
"last_touched_at": "2026-10-14T16:02:11+01:00",
"created_at": "2026-10-12T10:40:15+01:00",
"updated_at": "2026-10-14T16:02:11+01:00"
}
}
statusis the lead's outcome:openwhile it's being worked,won, orlostand the ways a lead can be ruled out (disqualified,duplicate,invalid,not_interested,ineligible).terminalistrueonce it's final.stageis where it sits in your firm's pipeline.first_touched_atis when someone first contacted them.- It doesn't include the person's details, since you sent them.
external_system_idis the lead's id in your firm's own CRM, if it has one.
Keep up with all your leads#
To keep your own system in step, list the leads that have changed since you last asked, a page at a time. It's a feed of changes, so a lead comes round again each time it changes.
curl "https://subcontext.com/api/v1/crm/leads?limit=100" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{ "contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc", "status": "won", "terminal": true,
"stage": { "key": "won", "name": "Won", "kind": "won", "probability": 100 },
"updated_at": "2026-10-14T16:02:11+01:00" }
],
"next_cursor": "MjAyNi0xMC0xNFQxNTowMjoxMS4wMDAwMDBafDAxOTlhM2MyLTgyMDAtNzUyMi04ZWEzLTI3MzhhOWJhY2JkYw"
}
Pass next_cursor back as cursor for the next page, as with every list in
the API (see Conventions).
A page with fewer leads than limit is the last one for now. Save the
cursor and start from it next time to get only what has changed. An empty
page gives back the cursor you sent, so you can keep the latest
next_cursor. To start from a time instead of the beginning, send
updated_since (ISO 8601) without a cursor. Filter with status=won,lost
or campaign_id=.... A status that doesn't exist, such as a typo, answers
422 invalid_parameter rather than every lead.
Follow what happens to them#
For more detail, the event feed says what happened to your leads and when: stage changes, outcomes, owners assigned, calls and messages.
curl "https://subcontext.com/api/v1/crm/events?cursor=MDE5OWEzYzItODQwMC03NzQ0LThhYzUtNDliYWNiZGNlZGZl" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [
{ "id": "0199a3c2-8401-7855-9bd6-5acbdcedfe0f", "event": "lead.status_changed",
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc", "campaign_id": "0199a3c2-8300-7633-9fb4-38a9bacbdced",
"occurred_at": "2026-10-14T16:02:11+01:00", "detail": { "from": "open", "to": "won" } }
],
"next_cursor": "MDE5OWEzYzItODQwMS03ODU1LTliZDYtNWFjYmRjZWRmZTBm"
}
Pass next_cursor back as cursor to carry on. Filter with contact_id or
event (for example event=lead.stage_changed,lead.status_changed). A kind of
event the feed doesn't record answers 422 invalid_parameter.
Events say what changed but not what was said or written. Use them as a cue to get the lead. They're kept for your firm's retention period, so poll at least that often. Daily is enough for most firms.
Report what happened next#
When your own systems know more (the lead bought a policy elsewhere, or
cancelled), report it with the outcomes API (see Report outcomes).
An application with crm can report outcomes for the leads it sent.