Collection webhooks
Get notified the moment a collection reaches a final state, instead of polling the status endpoint. A webhook is a lightweight signal — it tells you which collection the event is about and how it ended, so you can fetch the result from the Insurely API.
One endpoint receives every event type. Each delivery carries an eventType so you can tell them apart:
eventType | When it is sent |
|---|---|
COLLECTION_STATUS | A collection reached a terminal status. |
COLLECTION_TRANSACTION_HISTORY | The full transaction history for a wealth collection has been fetched. Only sent if full transaction history is enabled for your client. |
How it works
You register an HTTPS endpoint and receive a signing secret. A customer admin can do this in the Insurely Hub, on your client's Webhook tab, which also shows the secret. Without Hub access, send the endpoint to support@insurely.com.
When one of your collections reaches a terminal status, or its transaction history is ready, we POST a small JSON event to your endpoint, signed with your secret.
You verify the signature, respond 2xx quickly, and then fetch the result: GET /collections/{collectionId}/status (and the data endpoints) for a COLLECTION_STATUS event, GET /collections/{collectionId}/wealth/transactions for a COLLECTION_TRANSACTION_HISTORY event.
Webhooks complement the /status endpoint — they don't replace it. Treat the webhook as a "go fetch" signal; /status remains the source of truth.
The payload
Every delivery is a JSON body with these fields:
| Field | Type | Description |
|---|---|---|
collectionId | string | The collection the event is about. Use it to fetch via the API. |
eventType | string | COLLECTION_STATUS or COLLECTION_TRANSACTION_HISTORY. Decides what status means. |
status | string | For COLLECTION_STATUS: the terminal status (see the table below). For COLLECTION_TRANSACTION_HISTORY: COMPLETED or FAILED. |
eventId | string (UUID) | Unique id for this event. Use it to de-duplicate (see Idempotency). |
timestamp | string (ISO-8601) | When the event was generated. |
POST https://your-endpoint.example.com/insurely-webhook
Content-Type: application/json
X-Insurely-Event-Id: 6f1c2e9a-3b1d-4a77-9b2e-2c0a8f3d4e55
X-Insurely-Event-Type: COLLECTION_STATUS
X-Insurely-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
{
"collectionId": "c0ffee00-1234-5678-9abc-def012345678",
"eventType": "COLLECTION_STATUS",
"status": "COMPLETED",
"eventId": "6f1c2e9a-3b1d-4a77-9b2e-2c0a8f3d4e55",
"timestamp": "2026-06-11T09:21:44.512Z"
}POST https://your-endpoint.example.com/insurely-webhook
Content-Type: application/json
X-Insurely-Event-Id: 0b7d4c1e-8a2f-4e0b-9c3d-5f6a7b8c9d0e
X-Insurely-Event-Type: COLLECTION_TRANSACTION_HISTORY
X-Insurely-Signature: 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae
{
"collectionId": "c0ffee00-1234-5678-9abc-def012345678",
"eventType": "COLLECTION_TRANSACTION_HISTORY",
"status": "COMPLETED",
"eventId": "0b7d4c1e-8a2f-4e0b-9c3d-5f6a7b8c9d0e",
"timestamp": "2026-06-11T09:24:02.118Z"
}Branch on eventType before reading status. With full transaction history enabled, a single collection produces two events at the same URL with the same collectionId: first COLLECTION_STATUS, then COLLECTION_TRANSACTION_HISTORY. A receiver that ignores eventType and sees "status": "FAILED" on the second event would wrongly mark the whole collection as failed, when only the transaction history fetch failed.
The payload deliberately carries no collected data or PII — fetch the collection from the API after you receive the event.
Headers
| Header | Description |
|---|---|
X-Insurely-Signature | Lowercase-hex HMAC-SHA256 of the exact request body, keyed by your signing secret. Verify this. |
X-Insurely-Event-Id | Same value as eventId in the body; your idempotency key. |
X-Insurely-Event-Type | Same value as eventType in the body, so you can route before parsing it. |
Content-Type | application/json |
Status values
COLLECTION_STATUS
A webhook is sent for every terminal status — i.e. any state where the collection will receive no further updates: success, failure, and states that need end-user action. The status field is one of:
| Group | Values |
|---|---|
| Completed | COMPLETED, COMPLETED_PARTIAL, COMPLETED_EMPTY |
| Failed | FAILED, FAILED_PDF_PARSE, FAILED_PDF_USER_INPUT, THIRD_PARTY_ERROR |
| Action needed | INCORRECT_CREDENTIALS, AUTHENTICATION_TIMEOUT, AUTHENTICATION_CANCELLED, AUTHENTICATION_CONFLICT, AUTHENTICATION_MISMATCH, TWO_FACTOR_METHOD_SELECTION_TIMEOUT, KYC_FORM, CONTACT_FORM, CUSTOMER_ENROLLMENT_REQUIRED, COLLECTION_INPUT_TIMEOUT |
Treat status as a stable, webhook-specific vocabulary. For the authoritative, fully-detailed status, call /status. New values may be added over time — handle unknown values gracefully.
COLLECTION_TRANSACTION_HISTORY
Sent once per wealth collection, after the collection itself has finished, when full transaction history is enabled for your client.
| Value | Meaning |
|---|---|
COMPLETED | The full transaction history is stored. Fetch it with GET /collections/{collectionId}/wealth/transactions. |
FAILED | The transaction history could not be fetched, or the collection itself did not complete. There is nothing to fetch; stop waiting. |
If this event never arrives for a collection, the transaction history was not available for that provider. Don't wait for it indefinitely.
Verifying the signature
Compute the HMAC-SHA256 of the raw request body bytes using your signing secret, render it as lowercase hex, and compare it (in constant time) to the X-Insurely-Signature header. Reject the request if it doesn't match.
const crypto = require("crypto");
function isValid(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8") // the EXACT received bytes — do not re-serialize
.digest("hex");
const a = Buffer.from(signatureHeader || "", "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hashlib, hmac
def is_valid(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")Verify against the raw body your framework received, not a re-serialized object — JSON re-encoding can change bytes (key order, whitespace) and break the signature.
Idempotency
Delivery is at-least-once: you may receive the same event more than once (e.g. if your endpoint is slow to acknowledge and we retry). De-duplicate on X-Insurely-Event-Id — process each id once.
A single collection can emit more than one webhook over its lifecycle: several COLLECTION_STATUS events (for example COMPLETED_PARTIAL then COMPLETED), and a COLLECTION_TRANSACTION_HISTORY event if enabled. Each is a distinct event with its own eventId, so don't collapse different events by collectionId.
Retries & failures
- Respond with any
2xxto acknowledge. Anything else — non-2xx, a timeout, or a connection error — is treated as a failed delivery. - Failed deliveries are retried with exponential backoff over several attempts. After they're exhausted we stop retrying that event; you can still reconcile via
/status. - We do not follow redirects, and we apply request timeouts — point the webhook directly at your final HTTPS endpoint.
- Acknowledge fast (do the heavy work asynchronously). A slow endpoint looks like a failure and gets retried.
Requirements & best practices
- Endpoint must be HTTPS and publicly reachable (no internal/private addresses).
- Always verify the signature before trusting an event.
- Branch on
eventTypebefore interpretingstatus. - De-duplicate on
eventId. - Use the webhook as a trigger to fetch; rely on
/statusas the source of truth. - Keep your signing secret secret. Contact us to rotate it if exposed.
Last updated on