REST API

Quote and sell your products, send leads, read what became of them and report outcomes with the Subcontext API.

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

sh
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 read and write.

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 same Idempotency-Key, quoting an application that already exists, and reading it later don't show it, so keep it.
  • A browser application sends that share_token in an X-Share-Token header 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's share_token, the answer is 404 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-Key is a request about the application the first request made, so a browser application sends its share_token then too. Without it the answer is 409 idempotency_replay_unproven and 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.

json
{
  "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).

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

sh
curl "https://subcontext.com/api/v1/crm/events?limit=100&cursor=MDE5OWEzYzItODQwMS03ODU1LTliZDYtNWFjYmRjZWRmZTBm" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
json
{
  "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 null rather 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 Requests with an X-RateLimit-Reset header saying when you can carry on (in seconds since 1970). The body is the usual error, with the code rate_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

Stuck, or something missing?

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

Contact us