Webhooks
This document describes organization webhook configuration: available events, management (create, list, delete), and delivery verification. These endpoints are scoped to your organization via your x-api-key token (see Retrieving Your Authentication Token).
A webhook is configured per event: an organization can have at most one URL configured for document.ingestion_completed, one for extraction.completed, and one for classification.completed.
Prerequisites
Section titled “Prerequisites”- An active DIMARC account.
- Administrator status in your organization.
- Your authentication token
x-api-key(see Retrieving Your Authentication Token)
Response Format
Section titled “Response Format”Configuration endpoints use the API’s common envelope:
{ "status": "success", "data": { } }{ "status": "error", "code": "webhook_not_found", "error": "Webhook not found" }Error codes: agent_not_found (organization not found), webhook_not_found, invalid_request (invalid or disallowed URL).
Events
Section titled “Events”document.ingestion_completed
Section titled “document.ingestion_completed”Emitted when processing of a document ingested through the API (POST /v2/agents/{reference}/documents) completes. Documents uploaded from the web app do not trigger this event (they are tracked through the UI).
{ "id": "01J...", "event": "document.ingestion_completed", "timestamp": "2026-07-22T10:15:30.000Z", "data": { "agent_reference": "01ABC...", "document_reference": "01XYZ...", "status": "ready", "name": "annual-report.pdf", "path": "finance/2026" }}{ "id": "01J...", "event": "document.ingestion_completed", "timestamp": "2026-07-22T10:16:05.000Z", "data": { "agent_reference": "01ABC...", "document_reference": "01XYZ...", "status": "error", "name": "annual-report.pdf", "path": "finance/2026" }}extraction.completed
Section titled “extraction.completed”Emitted when an asynchronous extraction (POST /v2/extractor/async or POST /v2/extractor/async/{agentReference}) completes, in addition to the per-request callback described in the Extractor documentation.
Success
Section titled “Success”{ "id": "01J...", "event": "extraction.completed", "timestamp": "2026-07-22T10:20:00.000Z", "data": { "status": "success", "extract_id": "01K74Z3Q8RVN5XW2B6JHMTGD9C", "data": [ { "invoice_number": "INV-2024-001", "total_amount": 1250.5 } ], "metadata": { "filename": "invoice.pdf", "duration": 3.45, "tokens_consumed": 1, "thread_id": "01J..." } }}Failure
Section titled “Failure”{ "id": "01J...", "event": "extraction.completed", "timestamp": "2026-07-22T10:20:00.000Z", "data": { "status": "error", "extract_id": "01K74Z3Q8RVN5XW2B6JHMTGD9C", "code": "agent_timeout", "error": "Agent response timed out", "detail": { "http_status": 504 } }}A detail.http_status below 500, such as 422 for no_data_extracted or infected_file, describes the submitted document, not a failure: do not retry the same extraction.
classification.completed
Section titled “classification.completed”Emitted when an asynchronous classification (POST /v2/classification/async) completes, in addition to the per-request callback described in the Classification documentation.
Success
Section titled “Success”{ "id": "01J...", "event": "classification.completed", "timestamp": "2026-07-22T10:20:00.000Z", "data": { "status": "success", "classification_id": "01K74Z3Q8RVN5XW2B6JHMTGD9C", "data": { "source_name": "document-batch.pdf", "mode": "document", "segments": [ { "start": 1, "end": 3, "label": "invoice", "title": "Invoice #2024-001", "confidence": 0.92 } ], "discovered_classes": [], "pages_count": 3 }, "metadata": { "filename": "document-batch.pdf", "duration": 4.12, "tokens_consumed": 1, "thread_id": "01J..." } }}Failure
Section titled “Failure”{ "id": "01J...", "event": "classification.completed", "timestamp": "2026-07-22T10:20:00.000Z", "data": { "status": "error", "classification_id": "01K74Z3Q8RVN5XW2B6JHMTGD9C", "code": "agent_timeout", "error": "Agent response timed out", "detail": { "http_status": 504 } }}Event Envelope
Section titled “Event Envelope”Every event delivered to your URL has the following structure:
| Field | Type | Description |
|---|---|---|
id |
string | Unique delivery identifier (ULID) — use it to deduplicate |
event |
string | document.ingestion_completed, extraction.completed, or classification.completed |
timestamp |
string | Emission time, ISO 8601 UTC |
data |
object | Event-specific payload, described above |
Configuring a Webhook
Section titled “Configuring a Webhook”Create or Update
Section titled “Create or Update”PUT /v2/organization/webhooks/{event}
{event}: document.ingestion_completed, extraction.completed, or classification.completed.
curl --request PUT 'https://api.dimarc.ai/v2/organization/webhooks/document.ingestion_completed' \ --header 'x-api-key: <your_api_key>' \ --header 'Content-Type: application/json' \ --data '{ "url": "https://your-domain.com/webhooks/dimarc" }'url(required): must be a publichttps://URL. URLs pointing to a private IP, loopback, or internal hostname are rejected (422 invalid_request).
First configuration — 201 response, the secret is returned only once:
{ "status": "success", "data": { "event": "document.ingestion_completed", "url": "https://your-domain.com/webhooks/dimarc", "secret": "whsec_dimarc_..." }}Updating the URL (webhook already configured for this event) — 200 response, without a secret:
{ "status": "success", "data": { "event": "document.ingestion_completed", "url": "https://your-domain.com/webhooks/dimarc-v2" }}Possible errors: 404 agent_not_found (organization not found), 422 invalid_request (invalid or disallowed URL).
Listing Configured Webhooks
Section titled “Listing Configured Webhooks”GET /v2/organization/webhooks
curl 'https://api.dimarc.ai/v2/organization/webhooks' \ --header 'x-api-key: <your_api_key>'{ "status": "success", "data": [ { "event": "document.ingestion_completed", "url": "https://your-domain.com/webhooks/dimarc", "created_at": "2026-07-01T09:00:00.000Z", "updated_at": "2026-07-01T09:00:00.000Z" } ]}The secret is never returned by this endpoint.
Possible errors: 404 agent_not_found (organization not found).
Deleting a Webhook
Section titled “Deleting a Webhook”DELETE /v2/organization/webhooks/{event}
curl --request DELETE 'https://api.dimarc.ai/v2/organization/webhooks/document.ingestion_completed' \ --header 'x-api-key: <your_api_key>'{ "status": "success", "data": { "event": "document.ingestion_completed" } }Possible errors: 404 agent_not_found (organization not found), 404 webhook_not_found (no webhook configured for this event).
Secret Rotation
Section titled “Secret Rotation”There is no dedicated rotation endpoint: a PUT on an already-configured webhook only changes the URL, it never regenerates the secret. To obtain a new secret:
DELETE /v2/organization/webhooks/{event}PUT /v2/organization/webhooks/{event}with the same URL (or a new one) — a new secret is generated and returned once.
Signature Verification
Section titled “Signature Verification”Every delivery carries an x-dimarc-signature header:
x-dimarc-signature: sha256=<hex HMAC-SHA256(secret, raw_body)>The signature is computed over the request’s raw body (the exact bytes sent, before any JSON parsing). Verify it before deserializing:
import { createHmac, timingSafeEqual } from "node:crypto";
const seenEventIds = new Set(); // replace with a persistent store (Redis, DB...)
function verifyDimarcWebhook(rawBody, signatureHeader, secret) { const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const received = Buffer.from(signatureHeader ?? "", "utf8"); const expectedBuf = Buffer.from(expected, "utf8");
if ( received.length !== expectedBuf.length || !timingSafeEqual(received, expectedBuf) ) { throw new Error("Invalid webhook signature"); }
const event = JSON.parse(rawBody);
// Recommended tolerance: 5 minutes, to guard against replay of an intercepted request const ageMs = Date.now() - new Date(event.timestamp).getTime(); if (Math.abs(ageMs) > 5 * 60 * 1000) { throw new Error("Webhook timestamp outside tolerance"); }
// Deduplicate by id — the same event can be delivered more than once (retries) if (seenEventIds.has(event.id)) { return null; // already processed, ignore silently } seenEventIds.add(event.id);
return event;}Key points:
- Constant-time comparison (
timingSafeEqual) — a plain string comparison is vulnerable to a timing attack. - Raw body — if your framework parses JSON before you get access to the raw body (e.g. Express’s default middleware), configure it to expose the unparsed buffer on this route.
- Deduplicate by
id— retries may deliver the same event twice; your handler must be idempotent. - Timestamp tolerance — 5 minutes is recommended, to reject a request replayed long after the fact without being overly strict about normal network latency.
Retry Policy
Section titled “Retry Policy”On delivery failure (unreachable URL, timeout, or a 429/5xx response), delivery is retried up to 3 times after the initial attempt, with increasing backoff:
| Attempt | Delay before attempt |
|---|---|
| 1 (initial) | — |
| 2 | 1 second |
| 3 | 10 seconds |
| 4 | 60 seconds |
A 4xx response other than 429 is treated as a permanent failure — delivery is not retried. Make sure your endpoint responds 2xx as soon as the signature is valid and the event accepted, even if its business processing is deferred.
After the last attempt, delivery is abandoned: there is no replay queue or manual redelivery. The webhook is a best-effort channel only — for a guaranteed status, combine it with polling (GET /v2/agents/{reference}/documents).
Per-request callback
Section titled “Per-request callback”The callback of an asynchronous request follows the same policy: same delays, 10 s per attempt, retried on 429, 5xx and network errors. It is signed with the secret of the organization webhook for the matching event, if one exists. The result can still be fetched with GET /v2/turns/{turn_id} (classification, extraction), even if every delivery failed.
Support and Assistance
Section titled “Support and Assistance”For any questions regarding webhooks, contact our support team at contact@dimarc.fr