Webhooks
Set the webhook URL, verify the signature and handle session events
Available on request. Contact IDCanopy for access; details may change. Test environment only.
The session tells you when it needs you or has finished, by webhook.
Set the URL
PUT /qes-onboarding/webhook with { "url": "https://app.example.com/hooks/qes" }. The URL must use
https and resolve to a public address; redirects are not followed.
The call replaces any earlier URL and returns a new signing_secret, shown only in this response.
Store it.
{
"url": "https://app.example.com/hooks/qes",
"signing_secret": "whsec_3q2-7wEXAMPLEonlyNotARealSecret0"
}Events
Each delivery is a POST to your URL with this body:
{
"event_id": "evt_9c4e1f0a7b2d3e5f6a8b9c0d",
"event_type": "session.awaiting_document",
"session_id": "qes_5f1c2a9e0b7d4c3a8e6f1b2c",
"status": "awaiting_document",
"reference": "contract-42",
"occurred_at": "2026-10-15T10:02:05Z"
}event_type | What to do |
|---|---|
session.awaiting_document | Read the session for the identification, prepare the PDF and upload it. |
session.completed | Read the session for the signature evidence and download the signed PDF. |
session.failed | Read failure_reason. |
session.cancelled | The session was cancelled. |
session.expired | The link expired before the session finished. |
The same event may arrive more than once: deduplicate on event_id.
Answer with any 2xx. Other answers are retried with backoff, up to 8 attempts.
Verify the signature
Every delivery carries X-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of
<t>.<raw body> with your signing secret. Compute it over the raw body as received, before parsing,
and compare in constant time.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Pending verification: the contract sets no tolerance for the age of
t. Rejecting old timestamps protects against replay; agree a window with IDCanopy before relying on one.