If your CRM or policy administration system can send webhooks, it can tell Subcontext what happened without anyone writing an API client. You set up an inbound custom integration, which gives you a delivery URL, and tell Subcontext where to find things in your events. Your system posts each event, signed, to that address.
What Subcontext can do with an event:
- Lead outcomes: a sale, a quote, commission paid, a cancellation. It's matched to the lead and recorded, or held in Outcomes to match if it can't tell which lead it's for.
- Policy updates (insurance): news about one of the policies Subcontext holds, recorded on that policy's timeline.
If you'd rather write code, the outcomes API does the same job and tells you straight away what became of each outcome. See the API section. The dashboard help article Sending outcomes from your CRM compares the two.
Set it up#
- Open Integrations, External systems and press New definition. Choose Inbound. (This can't be changed later.)
- Fill in the Event type path: where in each event its type is, as a dotted
path. If your events look like
{"type": "deal.won", ...}, it'stype; for{"event": {"name": "deal.won"}}it'sevent.name. - Leave Signature header empty to use
X-Integrations-Signature, or enter the header name your system sends. - Create the definition. On its page, press New Secret, give it a name and a long random value, and then Edit the definition to choose it as the Signing secret variable. Deliveries are refused until you do.
- Fill in Lead outcomes or Policy updates (below).
- Copy the Delivery URL from the integration's page into your system's webhook settings, with the same signing secret.
The delivery URL#
POST https://subcontext.com/api/v1/webhooks/integrations/custom_http/YOUR_CONNECTION_ID
Content-Type: application/json
X-Integrations-Signature: t=1791797415,v1=5f8a2c0d9b...e1
{"event_id":"evt_000123","type":"deal.won","deal":{ ... }}
Copy the address from the integration's page instead of building it. The last part is the integration's own id.
The body must be a single JSON object. Its shape is up to you, because the integration's settings say where to find each part.
Sign each delivery#
The signature is the same scheme Subcontext uses for its own webhooks:
X-Integrations-Signature: t=<unix seconds>,v1=<signature>
where the signature is the HMAC-SHA256, in lower-case hex, of the timestamp, a
full stop and the exact body you send, keyed with the signing secret:
HMAC_SHA256(secret, t + "." + body).
- Sign the exact bytes you send. Encode the JSON once, sign that string, and send that string.
tmust be within 5 minutes of the real time, so keep your server's clock right, and sign each attempt afresh when you retry.- Only
t=andv1=are read, separated by a comma.
If your system uses a different signing scheme that you can't change, email hello@subcontext.com before you build anything.
What you get back#
| Status | Meaning |
|---|---|
200 |
Accepted. It's processed a moment later, so a 200 doesn't mean the outcome was matched. |
400 |
The body isn't valid JSON. |
403 |
Refused: no signature, a malformed one, a timestamp more than 5 minutes out, a signature that doesn't match, or no signing secret chosen on the integration. |
404 |
No such integration, or it has been revoked. Copy the delivery URL again. |
429 |
Too many deliveries: more than 3,000 in 5 minutes to this integration's delivery URL, or more than 30,000 in 5 minutes from your address to all of them. Wait and send again. |
The body of each answer is empty. Every delivery, accepted or refused, is in Integration logs (direction "inbound") with the reason.
Send again after a 429, a 5xx or no answer. Events are recorded once if you
give each one an id (see event_id below), so repeats are safe.
Deliveries have their own limit, separate from the 300 requests in 5 minutes that applies to the rest of the API, so a burst (after a bulk update in your CRM, say) doesn't run into it. Each integration's delivery URL counts on its own.
Lead outcomes#
Under Lead outcomes, list the Outcome event types to record (such as
deal.won, deal.lost) and write an Outcome mapping: for each part of an
outcome, the dotted path to it in your event. Events of any other type are
accepted and ignored.
For events like this:
{
"event_id": "evt_000123",
"type": "deal.won",
"deal": {
"id": "DEAL-123",
"contact_id": "YOUR_CRM_CONTACT_ID",
"closed_at": "2026-10-12T15:04:05Z",
"product": "term_life",
"commission": "450.00"
}
}
the mapping is:
{
"lead": { "external_id": "deal.contact_id" },
"type": "sold",
"occurred_at": "deal.closed_at",
"deal_ref": "deal.id",
"product": "deal.product",
"event_id": "event_id",
"values": { "commission": "deal.commission" }
}
| Mapping key | What it points at |
|---|---|
lead |
How to find the lead. At least one of id (Subcontext's contact_id, which every lead and webhook we send you carries), external_id (your own id for the lead, once Subcontext knows it), email or phone. Email and phone only match when you add "match_on_details": true and they belong to exactly one lead. |
type |
The outcome type's key for every event, such as sold. |
type_column |
Instead of type, a path whose value names the outcome type, when one event type can be several outcomes. |
occurred_at |
When it happened. Required. |
deal_ref |
Your reference for the sale. A cancellation names the sale it cancels this way. |
product |
The product's code. |
reason |
For a cancellation, why (such as cooling_off). |
event_id |
Your id for this event. With it, the same event sent twice is recorded once. Without it, Subcontext uses a fingerprint of the whole body. |
note |
A note to keep with the outcome. |
values |
For each measure, by its key (such as commission), the path to an amount in pounds. |
The outcome types and measure keys are your firm's own: managers see them under CRM, Outcomes. Outcomes from webhooks go through the same checks as the outcomes API.
To make sure each outcome finds its lead, store the contact_id from the lead
or webhook Subcontext sent you, and send it back as lead.id.
Policy updates (insurance)#
Under Policy updates, list the Update event types and give Our policy id path: where in the event to find Subcontext's id for the policy. Each matching event is recorded on that policy's timeline, where lifecycle events can react to it. Events without a policy id at that path, or for a policy that isn't yours, are ignored.
Examples#
Each of these builds a sale event like the one above, signs it and posts it to your delivery URL:
-
send-event.mjs
Node: signs and sends a sale from your CRM to an inbound custom integration's delivery URL.
-
send_event.py
Python: signs and sends a sale from your CRM to an inbound custom integration's delivery URL.
-
send-event.php
PHP: signs and sends a sale from your CRM to an inbound custom integration's delivery URL.