IDCanopy Developers
QES identify and sign

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_typeWhat to do
session.awaiting_documentRead the session for the identification, prepare the PDF and upload it.
session.completedRead the session for the signature evidence and download the signed PDF.
session.failedRead failure_reason.
session.cancelledThe session was cancelled.
session.expiredThe 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.

On this page