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 finished and how, so you can fetch the result from the Insurely API.
How it works
You register an HTTPS endpoint with Insurely and receive a signing secret.
When one of your collections reaches a terminal status, we POST a small JSON event to your endpoint, signed with your secret.
You verify the signature, respond 2xx quickly, and then call GET /collections/{collectionId}/status (and the data endpoints) to retrieve the result.
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 that reached a terminal status. Use it to fetch via the API. |
status | string | The terminal status (see the table below). |
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-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
{
"collectionId": "c0ffee00-1234-5678-9abc-def012345678",
"status": "COMPLETED",
"eventId": "6f1c2e9a-3b1d-4a77-9b2e-2c0a8f3d4e55",
"timestamp": "2026-06-11T09:21:44.512Z"
}The payload deliberately carries no collected data or PII — fetch the collection from the Insurely 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. |
Content-Type | application/json |
Status values
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.
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 terminal webhook over its lifecycle (for example COMPLETED_PARTIAL then COMPLETED). 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.
- 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 Insurely to rotate it if exposed.
Last updated on