Everything we send goes to the account's webhook_url, falling back to your organisation default, signed the same way. No URL configured means no webhooks (polling still works).
| Event | Fires when |
|---|---|
api_account.index_ready |
An account's website finished its first crawl. |
connection.authorised |
A connected source completed its OAuth exchange, or a direct source was created and validated. |
connection.auth_failed |
An exchange failed, or a refresh token died during a scheduled sync. |
connection.disconnected |
You deleted one connected source. |
connection_type.revoked |
A connection type was withdrawn from your organisation, destroying every connection of it across your fleet. |
api_account.index_ready
Crawling is deliberately slow (anti-detection), so don't block on it. This event only applies to accounts created with a site_url.
When the first crawl finishes we POST:
Response fields
eventstringapi_account.index_ready for this webhook.api_account_idintegeruidstringconnection_idintegerpostsintegerchunksintegersynced_attimestampThe connection lifecycle events
The three connection.* events share one payload shape and go to the webhook URL of the customer the source belongs to. They are how you keep your own "connected sources" screen honest without polling.
| Field | Type | Description |
|---|---|---|
event |
string | connection.authorised, connection.auth_failed or connection.disconnected. |
api_account_uid |
string | The customer id you provisioned the account with. |
connection_id |
integer | Context Link's id for the connection. |
connection_type |
string | The type, in the same form you sent it (google_doc, youtube). |
auth_status |
string | The connection's authorisation state at the moment the event fired. |
reason |
string | Only on failures: why, truncated to 200 characters. Present as a support message, not as something to branch on. |
connection.auth_failed is the one that matters operationally. Refresh tokens die silently, and across a fleet of thousands that is invisible rot: we can see it, but you are the only one who can ask your customer to reconnect. Route it somewhere a human or an email sequence will act on, then send them through re-authorisation.
connection.disconnected fires as the row is destroyed, so by the time you receive it the source and its content are already gone. It carries the same shape with no reason.
connection_type.revoked
A connection type can be withdrawn from your organisation, either by an admin of yours on the Connection types screen or by us. When that happens every connection of that type across your entire fleet is destroyed. You get one org-level event, not one per customer.
This goes to your organisation default webhook URL, because it is not about one customer. Fanning it out as 1,204 signed deliveries in a burst would read as an outage at your end; instead, reconcile per customer at your own pace with GET .../connections.
Signatures and delivery
Every delivery carries these headers, shown in the examples rail:
Verifying the signature
The signature is HMAC-SHA256(signing_secret, "<timestamp>.<raw body>"), hex-encoded. Verify it against the raw request body before parsing, compare with a constant-time function, and reject a timestamp skewed more than 5 minutes from your clock.
A Ruby implementation of that check:
The same check in Node, using Express with the raw body preserved:
Delivery contract
Delivery is at least once for every event: under normal operation you get exactly one call, but a queue or crash at the wrong moment can repeat it. Key on (uid, event) for api_account.index_ready, and on (connection_id, event) for the connection events, and make your handler idempotent.
Return any 2xx directly to acknowledge. Anything else is a failure and is retried about five times with exponential backoff, including a redirect: 3xx is treated as a failure, not followed, so point the webhook at its final URL.
SSRF constraints on the webhook URL
Your endpoint must be a publicly routable http(s) URL. These are refused outright (logged and never retried) rather than delivered:
- any scheme other than
httporhttps; - a URL with embedded credentials (
https://user:pass@…); - a host resolving to loopback, private (RFC 1918), CGNAT, link-local or multicast space, IPv4 or IPv6.
The same check runs when you set the URL, so a bad target is a 422 at configuration time instead of a silent dead letter later.