API documentation

Webhooks

Receive signed document lifecycle events from exdata.

Webhook delivery

Configure webhook endpoints in the app to receive document lifecycle events without polling. exdata sends POST requests with JSON payloads to each active endpoint that subscribes to the event.

SettingDescription
Endpoint URLHTTPS URL in your system that accepts webhook POST requests.
Signing secretReveal-once secret generated when the endpoint is created. Store it in your receiver configuration.
Subscribed eventsDocument lifecycle events selected for the endpoint.
Delivery IDUnique per delivery and replay. Use it for receiver idempotency.
RetriesFailed deliveries are retried by the webhook queue. A replay creates a new delivery ID and timestamp.

Signature headers

Verify the signature before trusting the payload. Store delivery IDs you have processed so retries do not run the same automation twice.

HeaderTypeDescription
X-EventStringEvent type, such as document.completed.
X-DeliveryUUID stringUnique delivery ID. Use this for receiver idempotency.
X-SignatureStringHMAC-SHA256 signature generated from the JSON payload and endpoint secret.
X-TimestampUnix timestampDelivery timestamp. Reject stale deliveries according to your own tolerance window.
User-AgentStringWebhook client user agent, currently exdata-webhooks/1.0.
Verify in Node.js
import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/webhooks/exdata", express.raw({ type: "application/json" }), (req, res) => {
  const payload = req.body.toString("utf8");
  const signature = req.header("X-Signature") ?? "";
  const expected = crypto
    .createHmac("sha256", process.env.EXDATA_WEBHOOK_SECRET)
    .update(payload)
    .digest("hex");

  const valid = signature.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!valid) {
    return res.sendStatus(401);
  }

  const event = req.header("X-Event");
  const delivery = req.header("X-Delivery");
  const body = JSON.parse(payload);

  // Persist delivery before starting async work.
  return res.status(202).json({ accepted: true, event, delivery });
});

Event types

Subscribe only to the events your integration needs. Many integrations use document.completed for automation and keep failure or blocked events for support workflows.

EventWhen it is sentTypical use
document.queuedA document was accepted and extraction work was queued.Mark a local upload as accepted.
document.processingThe document entered a processing stage such as analysis or extraction.Update UI state without polling.
document.completedProcessing finished successfully.Read extraction fields and trigger downstream mapping.
document.failedProcessing failed.Open a support task or move the document to manual review.
document.blockedProcessing was intentionally not queued, commonly because the account lacked credits.Notify operators and resolve account state before retrying.
webhook.testManual test delivery sent from the app.Validate receiver URL, signature verification, and idempotency storage.

Payload shape

Document events include the current document resource under data.document. The shape matches the document response object from the endpoint reference.

document.completed
{
  "id": "1d9d0f2b-4eb8-4c94-a636-7a71f8b6a071",
  "type": "document.completed",
  "created_at": "2026-05-10T01:00:19.000000Z",
  "data": {
    "document": {
      "id": 123,
      "mode": "live",
      "status": "completed",
      "processing_stage": "completed",
      "processing_error": null,
      "blocked_reason": null,
      "scanner_status": "clean",
      "scanner_provider": "local_noop",
      "scanner_message": null,
      "scanned_at": "2026-05-10T01:00:02.000000Z",
      "processing_started_at": "2026-05-10T01:00:04.000000Z",
      "processed_at": "2026-05-10T01:00:18.000000Z",
      "filename": "invoice-re-2026-1048.pdf",
      "file_format": "pdf",
      "file_size": 240123,
      "additional_text": null,
      "additional_text_plain": null,
      "custom_types": ["invoice"],
      "requester": "accounts-payable",
      "locale": "en",
      "number_of_pages": 1,
      "extracted_text": "Invoice RE-2026-1048...",
      "extracted_text_plain": "Invoice RE-2026-1048...",
      "origin": "api",
      "ai_processing": true,
      "is_e_invoice": false,
      "thumbnail": "https://www.exdata.app/api/v1/documents/123/thumbnail",
      "previews": [
        {
          "id": 987,
          "filename": "page-1.png",
          "file_format": "png",
          "file_size": 94812,
          "preview": "https://www.exdata.app/api/v1/previews/987"
        }
      ],
      "extractions": {
        "document_number": {
          "value": "RE-2026-1048",
          "candidates": ["RE-2026-1048"]
        },
        "gross_amount": {
          "value": "1079.50",
          "candidates": ["Amount due EUR 1,079.50"]
        }
      },
      "latest_extraction_run": {
        "id": 456,
        "mode": "live",
        "source": "api",
        "status": "completed",
        "blocked_reason": null,
        "error_code": null,
        "error_message": null,
        "extraction_schema_version": "2026-05-17",
        "extractor_version": "document:2026-05-17",
        "ai_prompt_version": "document-ai:2026-05-17",
        "normalization_version": "base:2026-05-10",
        "started_at": "2026-05-10T01:00:04.000000Z",
        "completed_at": "2026-05-10T01:00:18.000000Z",
        "created_at": "2026-05-10T01:00:03.000000Z"
      },
      "created_at": "2026-05-10T01:00:00.000000Z",
      "updated_at": "2026-05-10T01:00:18.000000Z"
    }
  }
}
webhook.test
{
  "id": "5fb23752-663d-47a2-9f0d-7fd4b5b4c9bd",
  "type": "webhook.test",
  "created_at": "2026-05-10T01:05:00.000000Z",
  "data": {
    "test": true,
    "account_id": 42,
    "endpoint": {
      "id": 9,
      "name": "Production receiver"
    },
    "triggered_by_user_id": 17
  }
}

Payload fields

Use these tables as the receiver contract. The nested document, preview, extraction, and extraction-run objects are the same objects returned by the API endpoints.

Top-level fields

FieldTypeDescription
idUUID stringDelivery ID. Same value as X-Delivery.
typeStringEvent type. Same value as X-Event.
created_atDate-time stringTime the payload was created for this delivery or replay.
dataObjectEvent-specific payload data.

Document event data

FieldTypeDescription
data.documentDocument objectCurrent document state, including metadata, previews, extraction fields when completed, and latest_extraction_run. Every document field is defined in Response objects.
data.document.extractionsObject or nullNormalized extraction fields for completed documents. Every field key is defined in Extraction fields.
data.document.latest_extraction_runExtraction run or nullLatest extraction run metadata, including version fields and failure/blocking details.

Test event data

FieldTypeDescription
data.testBooleanAlways true for manual test deliveries.
data.account_idIntegerAccount ID that owns the webhook endpoint.
data.endpoint.idIntegerWebhook endpoint ID.
data.endpoint.nameStringWebhook endpoint name from the app.
data.triggered_by_user_idInteger or nullUser ID that sent the test delivery.

Receiver behavior

Your receiver should acknowledge only after it has verified the signature and safely recorded the delivery ID. Long-running work should be queued in your own system.

  • Return any 2xx response when the delivery has been accepted.
  • Return 4xx for permanent receiver-side rejection, such as an invalid signature.
  • Return 5xx or time out only when exdata should retry delivery.
  • Deduplicate by X-Delivery or the payload id.
  • Use mode on data.document to keep test documents out of production automation.