Subcontext sees a lead arrive, and everything that happens in its own conversations and forms. It only knows what happens after that, in your own CRM or a provider's systems, if you tell it. That includes the quote your adviser gave, the policy sold, the commission paid and the sale cancelled. Reporting those outcomes lets your firm's reports show what each campaign, source and advert earns.
The token you need#
Use a server-only application with read and write, and:
outcomes, for your firm's own systems, which can report on any lead in your organisation; orcrm, for a partner, which can only report on the leads it sent.
What you can send#
Your organisation's outcome types and the values each takes are set up in the dashboard, so ask for them first:
curl https://subcontext.com/api/v1/crm/outcomes/model \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"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"] } ]
}
}
- Each entry in
typesis atypeyou can send. A type that isn'trepeatablecan only happen once to a lead. - Its
valuesare the measures it takes. You must send therequiredones, and Subcontext works out thecalculatedones from the others. - A
moneyunit is in whole pence (14940is £149.40), as everywhere in the API.percentis a percentage (22.5) andcounta whole number. A money value that isn't a whole number of pence is refused rather than rounded. Only GBP is supported. - Name a product by its
key, any of itsaliases(your own codes) or its name.
Report an outcome#
curl -X POST https://subcontext.com/api/v1/crm/outcomes \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"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 }
}'
| Field | What it is |
|---|---|
event_id |
Your own id for this outcome. Strongly recommended, because it makes sending the same one again safe. |
type |
The outcome type's key. |
occurred_at |
When it happened, ISO 8601. Reports count it in that month. |
lead |
Which lead (see below). |
values |
By measure key, in each measure's unit (money in whole pence). |
deal_ref |
Your reference for the deal or policy. A cancellation finds its sale by it. |
product |
Which product, by key, alias or name. |
note |
A note for your team. |
Name the lead in one of these ways, most certain first:
"lead": { "id": "..." }: Subcontext's id for the lead (itscontact_id, sent out with every lead and returned when you send one)."lead": { "external_id": "CRM-90211" }: your system's id for it."lead": { "email": "ada@example.com", "match_on_details": true }(orphone), only when you have no id. It only matches when exactly one person has those details.
Send a batch#
Send up to 500 at once as { "outcomes": [ ... ] }. Each gets its own result,
in the order sent:
curl -X POST https://subcontext.com/api/v1/crm/outcomes \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"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 } }
]
}'
{
"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"
}
]
}
}
The request as a whole answers 200 whatever happened to each outcome, so
check each result's status:
status |
What it means | What to do |
|---|---|---|
recorded |
Matched to its lead and recorded. | Nothing. |
duplicate |
An outcome with this event_id was already recorded. |
Nothing. It's safe to resend. |
held |
No single lead matched. It waits in your team's Review queue to be matched by hand. | Nothing, or send it again with a better identifier. |
invalid |
It can never be recorded as sent, for example because of an unknown type or product, a time that isn't ISO 8601, or a money value that isn't a whole number of pence. error says which. |
Fix it and send it again. |
conflict |
It's valid, but clashes with an outcome the lead already has (for example a second sale of a type that happens once). | Check what's recorded and reverse the wrong one if needed. |
Only a batch that's empty or over 500 is refused as a whole (422).
Cancellations and clawbacks#
When a sale is cancelled later, send a cancelled outcome dated when it was
cancelled, with the sale's deal_ref (it can be left out if the lead has
only one sale), a reason, and any commission clawed back:
{
"event_id": "cancel-77812",
"type": "cancelled",
"occurred_at": "2026-12-02T09:00:00Z",
"lead": { "id": "0199a3c2-8200-7522-8ea3-2738a9bacbdc" },
"deal_ref": "DEAL-5521",
"reason": "Cancelled within the cooling-off period",
"values": { "clawback": 26892 }
}
The sale still counts in its own month. The cancellation and clawback count in December.
Correct a mistake#
To undo an outcome sent in error, reverse it by the event_id you sent it with
(or by Subcontext's id, with Reverse an outcome):
curl -X POST https://subcontext.com/api/v1/crm/outcomes/reverse \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "event_id": "sale-77812", "note": "Recorded against the wrong customer." }'
{
"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"
}
}
The answer is the reversal, an outcome of its own that points at the one it
reverses in reversal_of. The record keeps both, and reports stop counting
it. To correct an outcome, reverse it and send the right one with a new
event_id. Reversing one that's already reversed answers 409 already_reversed.
Use a reversal only for mistakes. For a sale that was cancelled, send a cancellation, so the sale still counts in its month.
Read back what's recorded#
List outcomes gives the
outcomes recorded, oldest first, a page at a time (pass next_cursor back as
cursor, as every list in the API pages). Filter by contact_id, deal_ref
or type. A type that isn't one of yours answers 422 invalid_parameter.
The list includes outcomes your firm recorded in the dashboard or from spreadsheets, and the ones
Subcontext records itself (such as quoted and sold for policies sold
through it).