Send leads and read what happened to them

Send leads from your own systems, check each one was taken in, and keep up with where your leads are and what has happened to them.

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#

sh
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:

json
{
  "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:

sh
curl https://subcontext.com/api/v1/crm/ingests/0199a3c2-8100-7411-9d92-162738a9bacb \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
json
{
  "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:

sh
curl https://subcontext.com/api/v1/crm/leads/0199a3c2-8200-7522-8ea3-2738a9bacbdc \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
json
{
  "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"
  }
}
  • status is the lead's outcome: open while it's being worked, won, or lost and the ways a lead can be ruled out (disqualified, duplicate, invalid, not_interested, ineligible). terminal is true once it's final.
  • stage is where it sits in your firm's pipeline. first_touched_at is when someone first contacted them.
  • It doesn't include the person's details, since you sent them.
  • external_system_id is 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.

sh
curl "https://subcontext.com/api/v1/crm/leads?limit=100" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
json
{
  "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.

sh
curl "https://subcontext.com/api/v1/crm/events?cursor=MDE5OWEzYzItODQwMC03NzQ0LThhYzUtNDliYWNiZGNlZGZl" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
json
{
  "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.

Stuck, or something missing?

Get in touch and a person on the team will help.

Contact us