Skip to content

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.

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).

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"
}
}

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.

{
"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..."
}
}
}
{
"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.

Emitted when an asynchronous classification (POST /v2/classification/async) completes, in addition to the per-request callback described in the Classification documentation.

{
"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..."
}
}
}
{
"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 }
}
}

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

PUT /v2/organization/webhooks/{event}

{event}: document.ingestion_completed, extraction.completed, or classification.completed.

Fenêtre de terminal
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 public https:// 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).

GET /v2/organization/webhooks

Fenêtre de terminal
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).

DELETE /v2/organization/webhooks/{event}

Fenêtre de terminal
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).

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:

  1. DELETE /v2/organization/webhooks/{event}
  2. PUT /v2/organization/webhooks/{event} with the same URL (or a new one) — a new secret is generated and returned once.

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.

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).

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.

For any questions regarding webhooks, contact our support team at contact@dimarc.fr