CRM: outcomes

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.

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

FieldTypeDescription
event_id required string

The event_id you sent the outcome with.

note string

Why, for your team (up to 500 characters).

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

FieldTypeDescription
data required Outcome

An outcome recorded against a lead.

data.id required string (uuid)
data.event_id required string or null

The event_id it was sent with.

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 api, upload or manual.

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.

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

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"
  }
}

409 `already_reversed`: the outcome was already reversed, or is itself a reversal.

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": "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.

FieldTypeDescription
data required object
data.types required array of object

The outcome types, each a type you can send.

data.types[].key required string

What to send as type.

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 product.

One of: "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 values.

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 in whole pence (24950 is £249.50), percent as a percentage (22.5), count as a whole number.

One of: "money", "count", "percent"
data.products required array of object

Your products, for product on an outcome.

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.

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

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"
  }
}

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

NameInTypeDescription
id required path string (uuid)

The outcome's id.

Request body application/json

FieldTypeDescription
note string

Why, for your team (up to 500 characters).

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

FieldTypeDescription
data required Outcome

An outcome recorded against a lead.

data.id required string (uuid)
data.event_id required string or null

The event_id it was sent with.

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 api, upload or manual.

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.

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

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"
  }
}

409 `already_reversed`: the outcome was already reversed, or is itself a reversal.

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

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 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.

FieldTypeDescription
data required array of Outcome

The outcomes, oldest first.

data[].id required string (uuid)
data[].event_id required string or null

The event_id it was sent with.

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 api, upload or manual.

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 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-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.

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`: `type` isn't one of your outcome types, 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": "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 this event_id was 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_id names it).
  • invalid: it can never be recorded as sent (error says why).
  • conflict: it's valid, but clashes with an outcome the lead already has (error says 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

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

FieldTypeDescription
data required object
data.counts required object

How many outcomes got each result, such as { "recorded": 1, "held": 1 }. Results with none are left out.

data.results required array of object

One for each outcome sent, in order.

data.results[].event_id string

The event_id you sent, if you sent one.

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 event_id it was sent with.

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 api, upload or manual.

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.

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

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 `validation_failed`: send between 1 and 500 outcomes.

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": "Send between 1 and 500 outcomes.",
    "fields": {
      "outcomes": "Send between 1 and 500 outcomes."
    }
  }
}

Stuck, or something missing?

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

Contact us