Receive webhooks
Use webhooks to react to asynchronous Document and Assessment completion without continuously polling Dockt.
Prerequisites
Section titled “Prerequisites”You need:
- A public HTTPS endpoint that can read the raw request body.
- A workspace credential with
webhooks:manage. - A secret manager for the one-time webhook signing secret.
Export your API credentials:
export DOCKT_API_BASE_URL='https://api.dockt.com'export DOCKT_API_TOKEN='REPLACE_WITH_BEARER_TOKEN'1. Create a webhook endpoint
Section titled “1. Create a webhook endpoint”Subscribe to the events your integration handles:
webhook_response="$( curl --fail-with-body --silent --show-error \ -X POST "$DOCKT_API_BASE_URL/v1/webhooks" \ -H "Authorization: Bearer $DOCKT_API_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: webhook-production-events' \ --data '{ "label": "Production events", "url": "https://example.com/webhooks/dockt", "events": [ "document.completed", "document.failed", "assessment.completed" ] }')"
export DOCKT_WEBHOOK_ID="$(printf '%s' "$webhook_response" | jq -r '.data.id')"printf '%s' "$webhook_response" | jq '.data | {id, status, events, secret}'Store data.secret immediately. Dockt returns it only in the create response.
2. Read the raw request
Section titled “2. Read the raw request”Dockt sends an HTTP POST with Content-Type: application/json and these headers:
X-Dockt-Event-IdX-Dockt-Event-TypeX-Dockt-TimestampX-Dockt-Signature
Read the exact body bytes before parsing JSON. Re-serializing parsed JSON changes the signed message and causes signature verification to fail.
3. Verify the signature
Section titled “3. Verify the signature”The signed message is <timestamp>.<raw body>. The signature is a lowercase hexadecimal HMAC-SHA256 digest prefixed with v1=.
This Web Crypto implementation rejects malformed timestamps, requests outside the five-minute replay window, and invalid signatures:
export const verifyDocktWebhook = async (input: { body: string; secret: string; signature: string; timestamp: string; now?: Date;}) => { const signedAt = Date.parse(input.timestamp); const now = input.now?.getTime() ?? Date.now(); if (!Number.isFinite(signedAt) || Math.abs(now - signedAt) > 5 * 60 * 1000) { return false; }
const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', encoder.encode(input.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const digest = await crypto.subtle.sign( 'HMAC', key, encoder.encode(`${input.timestamp}.${input.body}`) ); const expected = `v1=${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
if (expected.length !== input.signature.length) return false;
let mismatch = 0; for (let index = 0; index < expected.length; index += 1) { mismatch |= expected.charCodeAt(index) ^ input.signature.charCodeAt(index); } return mismatch === 0;};Use your platform’s constant-time comparison function when it provides one.
4. Connect verification to your HTTP handler
Section titled “4. Connect verification to your HTTP handler”Pass verified events to a function that stores the event ID and queues your application work:
type AcceptEvent = (input: { eventId: string; event: unknown;}) => Promise<'stored' | 'duplicate'>;
export const receiveDocktWebhook = async ( request: Request, secret: string, acceptEvent: AcceptEvent) => { const eventId = request.headers.get('x-dockt-event-id'); const signature = request.headers.get('x-dockt-signature'); const timestamp = request.headers.get('x-dockt-timestamp');
if (!eventId || !signature || !timestamp) { return new Response('Missing webhook headers', { status: 400 }); }
const body = await request.text(); const valid = await verifyDocktWebhook({ body, secret, signature, timestamp }); if (!valid) return new Response('Invalid signature', { status: 401 });
let event: unknown; try { event = JSON.parse(body); } catch { return new Response('Invalid JSON', { status: 400 }); }
await acceptEvent({ eventId, event }); return new Response(null, { status: 204 });};Implement acceptEvent in your application so it inserts eventId with a unique constraint and durably stores or queues the event in the same operation. Return duplicate when the ID already exists. A duplicate still receives 204 because your application already accepted it.
5. Deduplicate before side effects
Section titled “5. Deduplicate before side effects”Delivery is at least once and unordered. Store X-Dockt-Event-Id with a unique constraint before applying a side effect. A retry uses the same event ID and body.
Don’t assume that a document.completed event arrives before assessment.completed, or that events for separate Documents arrive in upload order. Fetch the canonical resource named by the event before applying an important state change.
6. Acknowledge after durable acceptance
Section titled “6. Acknowledge after durable acceptance”Return any 2xx response after you have stored the event or queued durable work. Non-2xx responses and timeouts are retried up to 10 total attempts, with exponential delays capped at about 60 seconds. The default delivery timeout is 10 seconds.
Keep request handling short. Move long-running work out of the HTTP request.
7. Handle each event
Section titled “7. Handle each event”document.completed: FetchGET /v1/documents/{documentId}for full facts, checks, issues, and Findings.document.failed: Fetch the Document, record the failure, and route it to retry or review.assessment.completed: Fetch the Assessment or the provideddecision_idbefore acting on the result.webhook.test: Confirm signature verification, deduplication, and acknowledgement without changing product state.
8. Send a test event
Section titled “8. Send a test event”Queue a targeted webhook.test delivery:
curl --fail-with-body --silent --show-error \ -X POST \ -H "Authorization: Bearer $DOCKT_API_TOKEN" \ "$DOCKT_API_BASE_URL/v1/webhooks/$DOCKT_WEBHOOK_ID/test" | jqThe webhook must be active. The API returns 202 Accepted when the test delivery is queued.
9. Monitor delivery health
Section titled “9. Monitor delivery health”Read GET /v1/webhooks/{webhookId} and inspect delivery:
last_event_typelast_delivery_statuslast_attempted_atlast_succeeded_at
These fields are null before the first delivery attempt. Alert when the latest status remains failed, and use canonical API reads to recover any missed workflow state.