The Subcontext API lets your own systems and pages work with your Subcontext organisation: read your products and their questions, quote and sell them, send leads in and follow them, and report what became of them. This page covers what every request has in common. The guides below walk through the common jobs, and the API reference lists every request with its fields, answers and errors.
The basics#
- Address: every request goes to
https://subcontext.com/api/v1/.... There is one environment, so use a test product or campaign to try things out. - Format: send JSON with
Content-Type: application/json, and you get JSON back. - Authentication: every request needs an access token, sent as
Authorization: Bearer YOUR_ACCESS_TOKEN. - Try it in your browser: the API console sends any request with your own API application, checks it against the docs and gives you the code for it. Use the API console says how.
- The machine-readable reference is at https://subcontext.com/developers/api/openapi.yaml (OpenAPI 3.1), and as JSON at https://subcontext.com/developers/api/openapi.json. Most API tools can import it.
curl https://subcontext.com/api/v1/products \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Access tokens#
Your code gets a token from https://subcontext.com/oauth/token with your API
application's client id and secret. A token lasts two hours. Reuse it until
shortly before it runs out (or until a request answers 401), then ask for a
new one. Access tokens covers this with examples in
several languages, and Get a token has the details.
curl -X POST https://subcontext.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d "scope=read write crm" \
-d resource=https://subcontext.com/api/v1
Each request needs certain scopes on the token. Reading (a GET) needs
read or write. Anything that changes something (a POST, PATCH or
PUT) needs write. The reference lists what each request needs under its
name. A token request that leaves out scope gets every scope the
application has; name some to get only those.
| Scope | What it's for |
|---|---|
read |
Reading your products, journeys, applications, quotes and memberships. |
write |
Everything that changes something: starting quotes and applications, saving answers, taking payment, setting up policies, sending leads, recording outcomes, loyalty updates. A token with write can read too. |
crm |
Sending leads and reading what became of the leads that application sent. |
outcomes |
Reporting outcomes for any lead in your organisation. |
Server-only and browser applications#
An API application is one of two kinds, and that decides where its tokens work. See Server-only or browser for how to set each up.
- A server-only application has no allowed origins. Its tokens work from your servers, and are refused if a browser sends them. It can have any scope.
- A browser application lists the websites (origins) it's used on. Its
tokens only work from pages on those websites, and only have
readandwrite.
Anyone who opens your page can get a browser token, so some requests are for server-only applications whatever the token's scopes:
| Part of the API | Server-only | Browser |
|---|---|---|
| Products, Journeys | Yes | Yes |
| Quotes and applications, Checkout | Yes | Yes |
| Lookups (phone numbers, postcodes) | Yes | Yes |
| CRM: leads, CRM: outcomes | Yes | No |
| Loyalty, Outreach | Yes | No |
A few things differ by kind even where both may call:
- An application's
share_token(the key the quote form and webchat use) is shown to a browser application only on the request that makes the application (Start an application, or Quote in one request when it makes a new one). A replay with the sameIdempotency-Key, quoting an application that already exists, and reading it later don't show it, so keep it. - A browser application sends that
share_tokenin anX-Share-Tokenheader with every request about an application that already exists. That covers reading it, saving to it, quoting it and every Checkout request on its quotes. An application's or quote's id isn't a secret, so the id alone doesn't reach it. Without the header, or with another application'sshare_token, the answer is404 not_found, the same as for an id that doesn't exist, so it never says whether an id is real. A server-only application doesn't need the header. - A replay with the same
Idempotency-Keyis a request about the application the first request made, so a browser application sends itsshare_tokenthen too. Without it the answer is409 idempotency_replay_unprovenand nothing changes (see Sending something only once). - Customers are found by email address, and anyone can type anyone's. A browser application never changes the record of a customer who already exists, unless its own application created them: the application is theirs, but what was typed is kept on the application, shown back and priced on. A server-only application updates the record, except a policyholder's, which only gains what it's missing.
- A quote's assessment
trace(the rules and rate tables behind the decision) is only shown to a server-only application. - Only a server-only application may choose a policy's start date and number when it sets it up.
Conventions#
These rules apply to every part of the API.
Answers come in data#
A successful answer has what you asked for in data: one record as an
object, a list as an array.
{
"data": {
"id": "0199a3c2-6a10-7c2e-8d41-5b7f0e9c3a21",
"slug": "express",
"name": "Quick quote"
}
}
When a request gives back more than one thing, data is an object naming
each, such as data.application and data.quote from
Quote in one request.
Nothing else sits beside data, except next_cursor on a list that comes in
pages.
Errors#
Every error has the same shape: a short code your code can act on, a
message for a person, which says what to do, and, when the error is about
particular inputs, fields naming each one and saying why. A nested input
is named with a dot (customer.email), one in a list with its place
(components[0].term_years).
{
"error": {
"code": "answers_invalid",
"message": "invalid answers: cigarettes_per_day must be at most 100",
"fields": { "cigarettes_per_day": "must be at most 100" }
}
}
The status says what kind of problem it is:
| Status | What it means |
|---|---|
400 |
The body isn't JSON at all (invalid_json). |
401 |
No token, or the token has run out or was revoked (invalid_token). Get a new token and try once more. |
402 |
The card payment for a policy hasn't gone through yet (payment_not_succeeded). |
403 |
The token can't be used here because of a missing scope, the wrong kind of application, the wrong website, or a key (such as a share_token) that isn't this record's (forbidden). The message says which. |
404 |
No such record in your organisation, or nothing by the id you named inside the request, such as a market quote's result_id (not_found). For leads, also a lead this application didn't send. |
409 |
The request is fine but the record's state won't allow it now, for example an expired quote or a paused campaign. It may work later. |
422 |
The request can never work as sent. The message (and fields) say what to change. |
429 |
Too many requests (rate_limited). See Rate limits. |
503 |
Something the request needs isn't set up yet, such as your firm's payments (payments_unavailable). Try again once it is. |
These codes mean the same wherever they appear:
| Code | Status | Meaning |
|---|---|---|
invalid_token |
401 |
The token is missing, expired, revoked or not whole. |
forbidden |
403 |
This token, application or key can't do this. |
not_found |
404 |
No such record (or nothing by an id the request names). |
validation_failed |
422 |
The body is missing something required, or has a value that can't be used. fields names it. |
invalid_parameter |
422 |
A query parameter (a filter, a cursor, a time) has a value the API doesn't accept. A misspelt lead status, for example, is refused rather than ignored. fields names it. |
idempotency_key_conflict |
409 |
The Idempotency-Key was used before with different inputs. |
idempotency_replay_unproven |
409 |
A browser application sent an Idempotency-Key that already made an application, without that application's share_token. Send it in X-Share-Token, or start again with a new key. |
quote_not_bindable |
409 |
The quote can't be bought any more because it was replaced, bought or rejected. |
quote_expired |
409 |
The quote has expired. Quote again. |
customer_missing |
422 |
The application needs its customer's details first. |
payments_unavailable |
503 |
Your firm's Stripe account can't take payments yet. |
rate_limited |
429 |
Too many requests. |
Codes particular to one request, such as components_invalid or
campaign_paused, are listed with it in the reference.
Errors and fixes explains every
token and access error and how to fix it.
Lists come a page at a time, by cursor#
Lists that can grow long come a page at a time, oldest first. Send limit
for the page size, and pass each page's next_cursor back as cursor for
the next. A cursor marks the place after the page's last item. Store it as it
is, and don't build or read it. An empty page gives back the cursor you
sent, so you can store the latest next_cursor and ask again later for
what's new. A page with fewer than limit items is the last for now.
curl "https://subcontext.com/api/v1/crm/events?limit=100&cursor=MDE5OWEzYzItODQwMS03ODU1LTliZDYtNWFjYmRjZWRmZTBm" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
"data": [ { "id": "0199a3c2-8402-7966-8ce7-6bdcedfe0f10", "event": "lead.stage_changed" } ],
"next_cursor": "MDE5OWEzYzItODQwMi03OTY2LThjZTctNmJkY2VkZmUwZjEw"
}
List your leads,
List what happened to your leads
and List outcomes all
page this way. Leads are a feed of what has changed (a lead that changes
comes round again), so they also take updated_since, a time to start from
instead of the beginning. Short lists, such as your products and a product's
journeys, come whole.
Money is whole pence#
Every amount of money is a whole number of pence (the smallest unit of the
product's currency), in a field whose name ends _pence:
sum_assured_pence: 25000000 is £250,000. Amounts you send, such as a sum
assured or an outcome's money values, are in pence too. Nothing is rounded on
the way in, and a value that isn't a whole number of pence is refused. An outreach stream's costs are metered in US dollars, so the API
converts them at the day's exchange rate and rounds to the nearest penny.
Dates and times are ISO 8601#
Times are ISO 8601 with their UK offset, such as
2026-10-12T10:15:00+01:00. Dates are YYYY-MM-DD. Times you send are
ISO 8601 too.
Spellings are British#
Field names and values are spelt the British way: normalised,
analysed_count, a cancelled status.
Ids, empty values and new fields#
- Ids are UUIDs. Many records also have a
display_id, the id with a short prefix (app_,quo_,pol_), which is what your team sees in the dashboard. - A field with no value is
nullrather than missing, except where the reference says a field is only included sometimes. - New fields can be added at any time, so ignore fields you don't recognise.
Sending something only once#
A request that timed out may still have worked. These requests are safe to send again:
| Request | How |
|---|---|
| Start an application | An Idempotency-Key header. The same key and inputs give back the same application, and the same key with different inputs is refused with 409. |
| Quote in one request | An Idempotency-Key header. The same key and inputs give back the same quote, and the same key with different inputs is refused with 409. |
| Send a lead | An Idempotency-Key header. The same key never makes a second lead. Within 10 minutes you get the first answer again, with Idempotent-Replay: true. |
| Report outcomes | Each outcome's event_id. One already recorded comes back as duplicate. |
| Send points and tiers | Each event's idempotency_key. One already recorded is skipped. |
| Set up the policy | Nothing extra. Once the policy is set up, asking again gives back the same policy. |
Use a value that's unique to the thing in your own system, such as your record's id, rather than a random one, so a retry after a restart still matches.
A browser application's tokens are effectively public, and a key can be
guessed, so its replay of Start an application or Quote in one request must
prove it holds the application the first request made: send that
application's share_token in X-Share-Token, as with any request about it.
Without it the answer is 409 idempotency_replay_unproven, before anything
is read or changed. The share_token came in the first answer, so if that
answer never arrived, send the request again with a new Idempotency-Key,
and the old key in replaces_idempotency_key. That starts a fresh
application and closes the first one (abandoned, reason
superseded_by_retry), so nobody follows it up. It's only closed if it was
made in the last hour, hasn't changed since, and is for the same product,
journey and customer; otherwise it's left as it was. A browser application
should use a random key (a UUID) for these two requests, made fresh for each
new application.
Rate limits#
- 300 requests in 5 minutes from one IP address, counting everything your server sends to Subcontext, token requests included.
- Over that, you get
429 Too Many Requestswith anX-RateLimit-Resetheader saying when you can carry on (in seconds since 1970). The body is the usual error, with the coderate_limited. - Reusing tokens and sending outcomes in batches keeps you well inside it. If you need more, email hello@subcontext.com.
Versions#
The version is in the address: /api/v1. New requests and new fields can
appear in version 1 at any time, so ignore what you don't recognise. We add to
the API without breaking it. If we ever need to make a change that would
break your integration, we'll tell you in advance at the email address on your
account.
Help#
Email hello@subcontext.com, with the time of the request and its address (never your client secret or a token).
In this section
- Quote and sell through the API Ask a product's questions, quote the customer, take their Direct Debit and first payment, and set up the policy, step by step.
- Send leads and read what happened to them Send leads from your own systems, check each one was taken in, and keep up with where your leads are and what has happened to them.
- Report outcomes Tell Subcontext what became of each lead after it left (quoted, sold, cancelled, commission paid) so your reports show what each campaign earns.
- Loyalty partners For a loyalty programme that runs alongside a firm's policies, confirm each member's enrolment, change their status, and send the points and tiers they earn.
- Outreach streams Follow an outreach stream's progress from your own systems, and get people into one by sending them as leads.
- Use the API console Try any API request from your browser with your own API application, check it against the docs and copy it as code.
- API reference Every request the Subcontext API answers, from its OpenAPI document.