CRM: leads

Send leads in from your own systems or a partner's, and read what became of them: where each lead is now, and a feed of what has happened to them.

Send leads in from your own systems or a partner's, and read what became of them: where each lead is now, and a feed of what has happened to them.

These need a server-only application with the crm scope. An application only sees the leads it sent. The guide Send leads and read what happened to them walks through it.

Send a lead

POST https://subcontext.com/api/v1/crm/ingests Try it

Token scopes: write and crm

Sends a lead: who the person is (identity), what you know about them, and where they came from. Server-only applications.

The answer is 202 Accepted. Subcontext has stored the lead, and works out who it's about and reads it in the background. Poll What became of a lead (the Location header has its address) to see how that went.

The body is open. data takes any JSON your system has, and Subcontext reads it. Values you want stored exactly as given go in declared. You need at least one of data, text, declared or identity.

Idempotency: send an Idempotency-Key header (your own record's id is ideal). The same key again within 10 minutes gives back the first answer, with an Idempotent-Replay: true header. After that it still never makes a second lead (duplicate is true). The same content from the same source within 5 minutes is also treated as a retry.

Parameters

NameInTypeDescription
Idempotency-Key header string

A value unique to this request in your system, so a retry doesn't do it twice.

Request body application/json

FieldTypeDescription
source required string

Your system's name, such as my_website or hubspot_webhook. Lower-case letters, digits, _, . and -, up to 64 characters. leadform is reserved.

campaign string

The key of the campaign the lead is for (in the dashboard, under CRM, Campaigns). Without it, only your organisation-wide rules apply.

identity object

Who the person is. Any of email, phone, name, first_name, last_name, external_id (their id in your system).

data object

Anything else you know, in any shape. Subcontext reads it.

text string

Free text about the lead (a message, notes), read the same way.

declared object

Values stored exactly as you give them, by field key, never interpreted. Values must be plain (not nested objects).

consent object

What the person agreed to, as your form recorded it: for example { "service": { "email": true }, "marketing": { "email": false }, "method": "web_form" }.

context object

What you know about the person's visit, such as source_url, referrer, utm_source or user_agent.

provenance ProvenanceInput

Where the customer was when they filled it in, for your reports on what brings people in: page_url (with its utm_ tags), referrer, page_title, locale, timezone, utm and click_ids (such as gclid). Everything is checked and tidied before it's kept, and personal details in addresses are removed.

occurred_at string (date-time)

When the lead happened, if not now (ISO 8601).

source_reference string

Your own reference for this lead, shown back to you.

Example request
{
  "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"
}

Responses

202 The lead is stored and processed in the background. Poll its address (in the `Location` header) for what became of it.

FieldTypeDescription
data required Ingest

A lead you sent, and what became of it. Fields with nothing to say are left out.

data.id required string (uuid)
data.status required string

received, resolving and extracting while it's being worked on. Then applied (it's on the lead contact_id), needs_review (a person must decide who it's about), held (it didn't meet the campaign's rules; a person can accept it), deferred (paused, carries on by itself), failed or discarded.

One of: "received", "resolving", "extracting", "applied", "needs_review", "deferred", "failed", "discarded", "held"
data.source required string
data.source_reference string

Your reference, if you sent one.

data.received_at required string (date-time)
data.processed_at string (date-time)

When it was finished with.

data.contact_id string (uuid)

The lead it was applied to. Use it with Get a lead.

data.facts_written required integer

How many facts about the person were recorded.

data.fields_proposed required integer

How many values were put to your team because they differ from what's held.

data.activities_written required integer
data.truncated required boolean

Whether the lead was too long to read in full.

data.retry_after string (date-time)

For a deferred lead, when it carries on.

data.error string

Why it was deferred or failed.

data.validation_failures array of object

For a held lead, which of the campaign's rules it didn't meet, in your field names.

data.validation_failures[].field required string
data.validation_failures[].rule required string or null
data.validation_failures[].reason required string or null
data.provenance object

Where the lead was captured, as Subcontext understood it from your provenance and context.

data.explanation required string

What happened, in words.

data.duplicate boolean

Only in the answer to Send a lead: true when this was the same lead as one already received, which is what you get back.

Example
{
  "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,
      "captured_at": "2026-10-12T09:40:12Z",
      "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"
    },
    "explanation": "Accepted; working out who this is about.",
    "duplicate": false
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

409 The campaign isn't taking leads (`campaign_paused` or `campaign_archived`). The lead itself may be fine. Send it again when the campaign is active.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "campaign_paused",
    "message": "this campaign is not accepting leads"
  }
}

422 The lead can never be accepted as sent: - `unknown_campaign`: no campaign with that key in your organisation. - `validation_failed`: the message says what's wrong, for example a missing or malformed `source`, nothing to go on, or more than 256 KB (`fields` names the input, when it's one).

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "validation_failed",
    "message": "source is required and must look like 'hubspot_webhook'",
    "fields": {
      "source": "source is required and must look like 'hubspot_webhook'"
    }
  }
}

What became of a lead

GET https://subcontext.com/api/v1/crm/ingests/{id} Try it

Token scopes: read and crm or write and crm

Where a lead you sent has got to: still being read, applied to a contact (contact_id, which is also the lead's id), held by your campaign's rules, or waiting for a person to decide who it's about. explanation says it in words. Server-only applications, and only the application that sent it can see it.

A lead is done when status is applied, needs_review, held, failed or discarded. Until then, poll every few seconds.

Parameters

NameInTypeDescription
id required path string (uuid)

The ingest's id, from Send a lead.

Responses

200 The ingest.

FieldTypeDescription
data required Ingest

A lead you sent, and what became of it. Fields with nothing to say are left out.

data.id required string (uuid)
data.status required string

received, resolving and extracting while it's being worked on. Then applied (it's on the lead contact_id), needs_review (a person must decide who it's about), held (it didn't meet the campaign's rules; a person can accept it), deferred (paused, carries on by itself), failed or discarded.

One of: "received", "resolving", "extracting", "applied", "needs_review", "deferred", "failed", "discarded", "held"
data.source required string
data.source_reference string

Your reference, if you sent one.

data.received_at required string (date-time)
data.processed_at string (date-time)

When it was finished with.

data.contact_id string (uuid)

The lead it was applied to. Use it with Get a lead.

data.facts_written required integer

How many facts about the person were recorded.

data.fields_proposed required integer

How many values were put to your team because they differ from what's held.

data.activities_written required integer
data.truncated required boolean

Whether the lead was too long to read in full.

data.retry_after string (date-time)

For a deferred lead, when it carries on.

data.error string

Why it was deferred or failed.

data.validation_failures array of object

For a held lead, which of the campaign's rules it didn't meet, in your field names.

data.validation_failures[].field required string
data.validation_failures[].rule required string or null
data.validation_failures[].reason required string or null
data.provenance object

Where the lead was captured, as Subcontext understood it from your provenance and context.

data.explanation required string

What happened, in words.

data.duplicate boolean

Only in the answer to Send a lead: true when this was the same lead as one already received, which is what you get back.

Example
{
  "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."
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

404 There's no such record in your organisation.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "not_found",
    "message": "Resource not found"
  }
}

List your leads

GET https://subcontext.com/api/v1/crm/leads Try it

Token scopes: read and crm or write and crm

The leads this application sent, and where each is now, oldest change first. A lead that changes comes round again, so this is a feed of what has changed. Server-only applications.

Page through with cursor, as every list in the API does. Pass each page's next_cursor to get the next, and keep the last one to ask "what's changed since?" next time. A page with fewer than limit leads is the last for now. To start from a time rather than the beginning, send updated_since (and no cursor).

Filter by status (one or more, comma separated) and campaign_id.

Parameters

NameInTypeDescription
status query string

Only leads with these outcomes, comma separated (for example won,lost): open, won, lost, dormant, disqualified, duplicate, invalid, not_interested or ineligible. Any other value is refused.

campaign_id query string (uuid)

Only leads for this campaign.

cursor query string

Carry on after the page this came from (its next_cursor). Leave it out to start at the beginning.

updated_since query string (date-time)

Start with the leads changed at or after this time (ISO 8601), instead of from the beginning. Ignored when you send cursor.

limit query integer

How many leads per page, 1 to 200.

Responses

200 A page of leads.

FieldTypeDescription
data required array of Lead

The leads, oldest change first.

data[].contact_id required string (uuid)

The lead's id.

data[].external_system_id required string or null

The lead's id in your organisation's own CRM, if it has one.

data[].status required string

The lead's outcome so far.

One of: "open", "won", "lost", "dormant", "disqualified", "duplicate", "invalid", "not_interested", "ineligible"
data[].terminal required boolean

Whether that's final (won, lost, or ruled out).

data[].stage required object
data[].stage.key required string
data[].stage.name required string
data[].stage.kind required string One of: "open", "won", "lost"
data[].stage.probability required integer or null

The chance of a sale from this stage, as a percentage.

data[].stage_entered_at required string (date-time)
data[].owner_assigned required boolean

Whether someone on your team owns it.

data[].score required integer

How promising the lead is, from 0 to 100.

data[].campaign_id required string or null (uuid)
data[].first_touched_at required string or null (date-time)

When your team first contacted them.

data[].last_touched_at required string or null (date-time)
data[].created_at required string (date-time)
data[].updated_at required string (date-time)
next_cursor required NextCursor

Pass as cursor for the next page. It's the place after this page's last item. An empty page gives back the cursor you sent (null if you sent none), so you can keep it and ask again later.

Example
{
  "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"
    }
  ],
  "next_cursor": "MjAyNi0xMC0xNFQxNTowMjoxMS4wMDAwMDBafDAxOTlhM2MyLTgyMDAtNzUyMi04ZWEzLTI3MzhhOWJhY2JkYw"
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

422 `invalid_parameter`: `cursor` isn't one the API gave you, `updated_since` isn't an ISO 8601 time, or `status` has a value that isn't a lead status. `fields` names it.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_parameter",
    "message": "cursor isn't one this API gave you: pass a page's next_cursor as it came.",
    "fields": {
      "cursor": "isn't one this API gave you: pass a page's next_cursor as it came"
    }
  }
}

Get a lead

GET https://subcontext.com/api/v1/crm/leads/{id} Try it

Token scopes: read and crm or write and crm

Where one lead you sent is now: its stage, its outcome, whether someone owns it, and its score. Find it by the contact_id from What became of a lead. Server-only applications, and only the leads this application sent.

It doesn't include the person's details, since you sent them.

Parameters

NameInTypeDescription
id required path string (uuid)

The lead's contact_id.

Responses

200 The lead.

FieldTypeDescription
data required Lead

Where a lead is now, without the person's details.

data.contact_id required string (uuid)

The lead's id.

data.external_system_id required string or null

The lead's id in your organisation's own CRM, if it has one.

data.status required string

The lead's outcome so far.

One of: "open", "won", "lost", "dormant", "disqualified", "duplicate", "invalid", "not_interested", "ineligible"
data.terminal required boolean

Whether that's final (won, lost, or ruled out).

data.stage required object
data.stage.key required string
data.stage.name required string
data.stage.kind required string One of: "open", "won", "lost"
data.stage.probability required integer or null

The chance of a sale from this stage, as a percentage.

data.stage_entered_at required string (date-time)
data.owner_assigned required boolean

Whether someone on your team owns it.

data.score required integer

How promising the lead is, from 0 to 100.

data.campaign_id required string or null (uuid)
data.first_touched_at required string or null (date-time)

When your team first contacted them.

data.last_touched_at required string or null (date-time)
data.created_at required string (date-time)
data.updated_at required string (date-time)
Example
{
  "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"
  }
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

404 There's no lead for that contact id that this application sent.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "not_found",
    "message": "No lead for that contact id."
  }
}

List what happened to your leads

GET https://subcontext.com/api/v1/crm/events Try it

Token scopes: read and crm or write and crm

A feed of what has happened to the leads this application sent, oldest first: stage changes, outcomes, owners assigned, messages and calls. Server-only applications.

Each event says what happened and to whom, but not what was said or written. Use it as a prompt to get the lead.

To keep up, pass the last page's next_cursor as cursor. Events are kept for your organisation's retention period, so poll at least that often.

Parameters

NameInTypeDescription
cursor query string

Carry on after the page this came from (its next_cursor). Leave it out to start at the beginning.

contact_id query string (uuid)

Only this lead's events.

event query string

Only these kinds of event, comma separated (for example lead.stage_changed,lead.status_changed). A kind the feed doesn't record is refused.

limit query integer

How many events per page, 1 to 500.

Responses

200 A page of events.

FieldTypeDescription
data required array of LeadEvent

The events, oldest first.

data[].id required string (uuid)
data[].event required string

What happened, such as lead.created, lead.stage_changed, lead.status_changed, lead.owner_assigned, or an activity such as activity.policy.bound.

data[].contact_id required string (uuid)
data[].campaign_id required string or null (uuid)
data[].occurred_at required string (date-time)
data[].detail required object

The change but not its content (such as { "from": "new", "to": "quoted" }).

data[].detail.from any
data[].detail.to any
data[].detail.stage any
data[].detail.kind any
data[].detail.fields any
data[].detail.reason any
data[].detail.status any
data[].detail.channel any
data[].detail.occurred_at any
next_cursor required NextCursor

Pass as cursor for the next page. It's the place after this page's last item. An empty page gives back the cursor you sent (null if you sent none), so you can keep it and ask again later.

Example
{
  "data": [
    {
      "id": "0199a3c2-8400-7744-8ac5-49bacbdcedfe",
      "event": "lead.stage_changed",
      "contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
      "campaign_id": "0199a3c2-8300-7633-9fb4-38a9bacbdced",
      "occurred_at": "2026-10-13T09:12:30+01:00",
      "detail": {
        "from": "new",
        "to": "quoted"
      }
    },
    {
      "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"
}

401 No token was sent, or it isn't valid because it has expired, was revoked (for example because the application's secret was rotated), or was cut short. Get a new token and try once more.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_token",
    "message": "The access token has expired. Ask for a new one."
  }
}

403 The token can't be used here. It's missing a scope, it was used from the wrong place (a server-only application's token in a browser, or a browser application's token from a server, another website or a request only server-only applications may make), or it wasn't asked for with `resource=https://subcontext.com/api/v1`.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "forbidden",
    "message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
  }
}

422 `invalid_parameter`: `event` has a kind of event the feed doesn't record, or `cursor` isn't one the API gave you. `fields` names it.

FieldTypeDescription
error required object
error.code required string

A short code for the kind of error, such as invalid_token, forbidden, not_found or validation_failed. Each code means one thing wherever it appears.

error.message required string

What went wrong and what to do about it, in plain English.

error.fields object

When the error is about particular inputs, which ones and why, by name: a nested one with a dot (customer.email), one in a list with its place (components[0].term_years). Left out when the error isn't about particular inputs.

Example
{
  "error": {
    "code": "invalid_parameter",
    "message": "event has \"lead.stage_change\", not an event this feed records.",
    "fields": {
      "event": "has \"lead.stage_change\", not an event this feed records"
    }
  }
}

Stuck, or something missing?

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

Contact us