Report what became of a lead after it left Subcontext: quoted, sold, cancelled, commission paid. Outcomes feed your organisation's reports on what each campaign and source earns.
These need a server-only application with the outcomes scope (any
lead in your organisation) or the crm scope (only the leads that
application sent). The guide Report outcomes
walks through it.
Reverse an outcome by your event id
POST
https://subcontext.com/api/v1/crm/outcomes/reverse
Try it
Token scopes:
write and outcomes or write and crm
Undoes an outcome you sent in error, found by the event_id you
sent it with. The record keeps both the outcome and its reversal,
and reports stop counting it. To correct one, reverse it and send it
again with a new event_id. A sale that was cancelled is an outcome of its own, of type cancelled, rather than a reversal.
Server-only applications. With only the crm scope, only
outcomes for the leads this application sent.
Request body application/json
| Field | Type | Description |
|---|---|---|
| event_id required | string |
The |
| note | string |
Why, for your team (up to 500 characters). |
{
"event_id": "sale-77812",
"note": "Recorded against the wrong customer."
}
Responses
200 The outcome is reversed. `data` is the reversal, an outcome of its own pointing at the one it reverses in `reversal_of`.
| Field | Type | Description |
|---|---|---|
| data required | Outcome |
An outcome recorded against a lead. |
| data.id required | string (uuid) | |
| data.event_id required | string or null |
The |
| data.type required | string | |
| data.contact_id required | string (uuid) | |
| data.occurred_at required | string (date-time) | |
| data.source required | string |
How it was recorded, such as |
| data.deal_ref required | string or null | |
| data.product required | string or null |
The product's key. |
| data.values required | object |
Its values, by measure key. Money in whole pence, percentages as percentages, counts as whole numbers. |
| data.missing_values required | array of string |
Required values it doesn't have yet. |
| data.currency required | string or null | |
| data.reversed required | boolean |
Whether it has been reversed. |
| data.reversal_of required | string or null (uuid) |
For a reversal, the outcome it reverses. |
{
"data": {
"id": "0199a3c2-8501-7a77-9df8-7cedfe0f1021",
"event_id": null,
"type": "sold",
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
"occurred_at": "2026-10-20T14:00:00+01:00",
"source": "api",
"deal_ref": "DEAL-5521",
"product": "level-term",
"values": {
"annual_premium": 14940,
"commission": 26892
},
"missing_values": [],
"currency": "GBP",
"reversed": false,
"reversal_of": "0199a3c2-8500-7966-8ce7-6bdcedfe0f10"
}
}
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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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`.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "not_found",
"message": "Resource not found"
}
}
409 `already_reversed`: the outcome was already reversed, or is itself a reversal.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "already_reversed",
"message": "This outcome is already reversed."
}
}
The outcomes you can send
GET
https://subcontext.com/api/v1/crm/outcomes/model
Try it
Token scopes:
read and outcomes or write and outcomes or read and crm or write and crm
Your organisation's outcome types (such as quoted, sold and
cancelled), the values each takes (such as annual_premium and
commission), and your products, so you know what to send. Your
organisation can add its own in the dashboard.
Server-only applications.
Responses
200 The outcome types, values and products.
| Field | Type | Description |
|---|---|---|
| data required | object | |
| data.types required | array of object |
The outcome types, each a |
| data.types[].key required | string |
What to send as |
| data.types[].label required | string | |
| data.types[].repeatable required | boolean |
Whether a lead can have this outcome more than once. |
| data.types[].product required | string |
Whether an outcome of this type names a "none", "optional", "required"
|
| data.types[].values required | array of object |
The values it takes. |
| data.types[].values[].key required | string |
A measure's key, to use in |
| data.types[].values[].required required | boolean |
Whether you must send it. |
| data.types[].values[].calculated required | boolean |
Whether Subcontext works it out from the others (no need to send it). |
| data.measures required | array of object |
The values outcomes can carry. |
| data.measures[].key required | string | |
| data.measures[].label required | string | |
| data.measures[].unit required | string |
"money", "count", "percent"
|
| data.products required | array of object |
Your products, for |
| data.products[].key required | string | |
| data.products[].name required | string | |
| data.products[].aliases required | array of string |
Other codes your systems use for it, which work too. |
{
"data": {
"types": [
{
"key": "quoted",
"label": "Quoted",
"repeatable": false,
"product": "optional",
"values": [
{
"key": "monthly_premium",
"required": false,
"calculated": false
},
{
"key": "annual_premium",
"required": false,
"calculated": false
}
]
},
{
"key": "sold",
"label": "Sold",
"repeatable": false,
"product": "optional",
"values": [
{
"key": "monthly_premium",
"required": false,
"calculated": false
},
{
"key": "annual_premium",
"required": false,
"calculated": false
},
{
"key": "commission",
"required": false,
"calculated": false
}
]
},
{
"key": "cancelled",
"label": "Cancelled",
"repeatable": false,
"product": "optional",
"values": [
{
"key": "clawback",
"required": false,
"calculated": false
}
]
}
],
"measures": [
{
"key": "monthly_premium",
"label": "Monthly premium",
"unit": "money"
},
{
"key": "annual_premium",
"label": "Annual premium",
"unit": "money"
},
{
"key": "commission",
"label": "Commission",
"unit": "money"
},
{
"key": "clawback",
"label": "Clawback",
"unit": "money"
}
],
"products": [
{
"key": "level-term",
"name": "Level Term Life",
"aliases": [
"LTL",
"LT01"
]
}
]
}
}
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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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`.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "forbidden",
"message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
}
}
Reverse an outcome
POST
https://subcontext.com/api/v1/crm/outcomes/{id}/reverse
Try it
Token scopes:
write and outcomes or write and crm
The same as Reverse an outcome by your event id, found by Subcontext's id for the outcome instead.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id required | path | string (uuid) | The outcome's |
Request body application/json
| Field | Type | Description |
|---|---|---|
| note | string |
Why, for your team (up to 500 characters). |
{
"note": "Duplicate of an earlier sale."
}
Responses
200 The outcome is reversed. `data` is the reversal, an outcome of its own pointing at the one it reverses in `reversal_of`.
| Field | Type | Description |
|---|---|---|
| data required | Outcome |
An outcome recorded against a lead. |
| data.id required | string (uuid) | |
| data.event_id required | string or null |
The |
| data.type required | string | |
| data.contact_id required | string (uuid) | |
| data.occurred_at required | string (date-time) | |
| data.source required | string |
How it was recorded, such as |
| data.deal_ref required | string or null | |
| data.product required | string or null |
The product's key. |
| data.values required | object |
Its values, by measure key. Money in whole pence, percentages as percentages, counts as whole numbers. |
| data.missing_values required | array of string |
Required values it doesn't have yet. |
| data.currency required | string or null | |
| data.reversed required | boolean |
Whether it has been reversed. |
| data.reversal_of required | string or null (uuid) |
For a reversal, the outcome it reverses. |
{
"data": {
"id": "0199a3c2-8501-7a77-9df8-7cedfe0f1021",
"event_id": null,
"type": "sold",
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
"occurred_at": "2026-10-20T14:00:00+01:00",
"source": "api",
"deal_ref": "DEAL-5521",
"product": "level-term",
"values": {
"annual_premium": 14940,
"commission": 26892
},
"missing_values": [],
"currency": "GBP",
"reversed": false,
"reversal_of": "0199a3c2-8500-7966-8ce7-6bdcedfe0f10"
}
}
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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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`.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "not_found",
"message": "Resource not found"
}
}
409 `already_reversed`: the outcome was already reversed, or is itself a reversal.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "already_reversed",
"message": "This outcome is already reversed."
}
}
List outcomes
GET
https://subcontext.com/api/v1/crm/outcomes
Try it
Token scopes:
read and outcomes or write and outcomes or read and crm or write and crm
The outcomes recorded, oldest first, reversals included. With only
the crm scope, only those for the leads this application sent.
Server-only applications.
Page through by passing the last page's next_cursor as cursor.
Filter by lead (contact_id), deal (deal_ref) or type.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| cursor | query | string | Carry on after the page this came from (its |
| contact_id | query | string (uuid) | Only this lead's outcomes. |
| deal_ref | query | string | Only outcomes for this deal. |
| type | query | string | Only outcomes of this type (its key). A key that isn't one of your outcome types is refused. |
| limit | query | integer | How many per page, 1 to 200. |
Responses
200 A page of outcomes.
| Field | Type | Description |
|---|---|---|
| data required | array of Outcome |
The outcomes, oldest first. |
| data[].id required | string (uuid) | |
| data[].event_id required | string or null |
The |
| data[].type required | string | |
| data[].contact_id required | string (uuid) | |
| data[].occurred_at required | string (date-time) | |
| data[].source required | string |
How it was recorded, such as |
| data[].deal_ref required | string or null | |
| data[].product required | string or null |
The product's key. |
| data[].values required | object |
Its values, by measure key. Money in whole pence, percentages as percentages, counts as whole numbers. |
| data[].missing_values required | array of string |
Required values it doesn't have yet. |
| data[].currency required | string or null | |
| data[].reversed required | boolean |
Whether it has been reversed. |
| data[].reversal_of required | string or null (uuid) |
For a reversal, the outcome it reverses. |
| next_cursor required | NextCursor |
Pass as |
{
"data": [
{
"id": "0199a3c2-8500-7966-8ce7-6bdcedfe0f10",
"event_id": "sale-77812",
"type": "sold",
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
"occurred_at": "2026-10-20T14:00:00+01:00",
"source": "api",
"deal_ref": "DEAL-5521",
"product": "level-term",
"values": {
"annual_premium": 14940,
"commission": 26892
},
"missing_values": [],
"currency": "GBP",
"reversed": false,
"reversal_of": null
}
],
"next_cursor": "MDE5OWEzYzItODUwMC03OTY2LThjZTctNmJkY2VkZmUwZjEw"
}
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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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`.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "forbidden",
"message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
}
}
422 `invalid_parameter`: `type` isn't one of your outcome types, or `cursor` isn't one the API gave you. `fields` names it.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "invalid_parameter",
"message": "type isn't one of your outcome types.",
"fields": {
"type": "isn't one of your outcome types"
}
}
}
Report outcomes
POST
https://subcontext.com/api/v1/crm/outcomes
Try it
Token scopes:
write and outcomes or write and crm
Reports what became of leads, with one outcome as the body or up to 500 as { "outcomes": [...] }. Server-only applications.
Each outcome names its lead by Subcontext's id (lead.id, the
contact_id sent with every lead), your system's id for it
(lead.external_id), or, if you set lead.match_on_details, its
email address or phone number. Each one gets its own result, in
order:
recorded: matched to its lead and recorded.duplicate: an outcome with thisevent_idwas already recorded. Nothing changed, so it's safe to send again.held: no single lead matched. It waits in your team's review queue to be matched by hand (submission_idnames it).invalid: it can never be recorded as sent (errorsays why).conflict: it's valid, but clashes with an outcome the lead already has (errorsays which).
Money is in whole pence (14940 is £149.40), as everywhere in the API. A money value that isn't a whole number of pence makes that outcome invalid. Percentages are percentages (22.5). With only
the crm scope, only the leads this application sent can match.
Request body application/json
{
"outcomes": [
{
"event_id": "sale-77812",
"type": "sold",
"occurred_at": "2026-10-20T14:00:00+01:00",
"lead": {
"id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc"
},
"deal_ref": "DEAL-5521",
"product": "LTL",
"values": {
"annual_premium": 14940,
"commission": 26892
}
},
{
"event_id": "sale-77813",
"type": "sold",
"occurred_at": "2026-10-20T15:30:00+01:00",
"lead": {
"external_id": "CRM-90211"
},
"values": {
"annual_premium": 21204
}
}
]
}
Responses
200 A result for each outcome, in the order sent, and a count of each result.
| Field | Type | Description |
|---|---|---|
| data required | object | |
| data.counts required | object |
How many outcomes got each result, such as |
| data.results required | array of object |
One for each outcome sent, in order. |
| data.results[].event_id | string |
The |
| data.results[].status required | string |
One of: "recorded", "duplicate", "held", "invalid", "conflict"
|
| data.results[].error | string |
Why it was held, refused or clashed. |
| data.results[].outcome | Outcome |
An outcome recorded against a lead. |
| data.results[].outcome.id required | string (uuid) | |
| data.results[].outcome.event_id required | string or null |
The |
| data.results[].outcome.type required | string | |
| data.results[].outcome.contact_id required | string (uuid) | |
| data.results[].outcome.occurred_at required | string (date-time) | |
| data.results[].outcome.source required | string |
How it was recorded, such as |
| data.results[].outcome.deal_ref required | string or null | |
| data.results[].outcome.product required | string or null |
The product's key. |
| data.results[].outcome.values required | object |
Its values, by measure key. Money in whole pence, percentages as percentages, counts as whole numbers. |
| data.results[].outcome.missing_values required | array of string |
Required values it doesn't have yet. |
| data.results[].outcome.currency required | string or null | |
| data.results[].outcome.reversed required | boolean |
Whether it has been reversed. |
| data.results[].outcome.reversal_of required | string or null (uuid) |
For a reversal, the outcome it reverses. |
| data.results[].submission_id | string (uuid) |
For a held outcome, its place in the review queue. |
{
"data": {
"counts": {
"recorded": 1,
"held": 1
},
"results": [
{
"event_id": "sale-77812",
"status": "recorded",
"outcome": {
"id": "0199a3c2-8500-7966-8ce7-6bdcedfe0f10",
"event_id": "sale-77812",
"type": "sold",
"contact_id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc",
"occurred_at": "2026-10-20T14:00:00+01:00",
"source": "api",
"deal_ref": "DEAL-5521",
"product": "level-term",
"values": {
"annual_premium": 14940,
"commission": 26892
},
"missing_values": [],
"currency": "GBP",
"reversed": false,
"reversal_of": null
}
},
{
"event_id": "sale-77813",
"status": "held",
"error": "no lead matches those identifiers",
"submission_id": "0199a3c2-8600-7a77-9df8-7cedfe0f1021"
}
]
}
}
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.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"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`.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "forbidden",
"message": "origin 'https://www.example.com' is not allowlisted for this OAuth application"
}
}
422 `validation_failed`: send between 1 and 500 outcomes.
| Field | Type | Description |
|---|---|---|
| error required | object | |
| error.code required | string |
A short code for the kind of error, such as |
| 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 ( |
{
"error": {
"code": "validation_failed",
"message": "Send between 1 and 500 outcomes.",
"fields": {
"outcomes": "Send between 1 and 500 outcomes."
}
}
}