Authentication
All requests authenticate with a Bearer API key. Keys belong to a single organisation, are shown in plaintext only once, when created, and can be revoked at any time: the next request with a revoked key returns 401. API access needs a plan with the REST API (Pro and above).
Tokens are `pdf_` followed by 40 characters. The dashboard only shows a short prefix, to tell keys apart.
Authorization: Bearer pdf_X8s3...e2Qa
- ›Server-only — never embed an API key in a browser bundle or mobile binary.
- ›Keys are created from the dashboard; the token is shown only then. Last use is recorded (at most once a minute) and visible there.
- ›Revoke a key from the dashboard or with `DELETE /api-keys/{id}/`; effective on the next request.
- ›After 20 invalid keys in a minute from the same IP address, every request with a key from that address gets `429 throttled` until the minute is over, valid keys included.
Templates
Templates — the PDF the engine fills
A template is your source PDF plus the labels (named rectangles with coordinates) that mark where values are drawn. Labels are drawn in the visual editor or sent with `POST /labels/bulk-upsert/`; `POST /templates/{id}/detect-fields/` reads the PDF and proposes them, without saving anything.
GET /templates/
List the templates of your organisation.
Cursor-paginated, 25 per page (`page_size` up to 200), newest first. `?search=` matches name and description.
POST /templates/ multipart/form-data
Upload a new PDF as a template.
Fields
-
name string · required Display name. -
description string · optional Internal note for your team. -
file file · required PDF file, up to 25 MB and 200 pages.
Page previews are generated in the background. `previews_ready` flips to `true` when they're done.
GET /templates/{id}/
Retrieve a template with its labels and page previews.
POST /templates/{id}/detect-fields/
Propose the fields the PDF seems to ask for, without saving them.
Reads the form fields the PDF already has, the boxes, cells and lines it draws, and leaders such as `Name ______`. Returns `{ fields, pages, complete }`: each field has `page`, `x`, `y`, `width`, `height`, `field_type`, `layout`, `box_count`, `line_count`, `font_size`, `description` and `source`. Keep the right ones and save them with `bulk-upsert`. `complete` is `false` when the reading stopped before the last page. At most 30 calls an hour.
DELETE /templates/{id}/
Delete the template and its files.
Refused with `409 template_in_use` while compilations, batches or share links still reference it.
POST /labels/bulk-upsert/ application/json
Replace the labels, groups and signers of a template in one call, and its requested attachments and consents when sent.
Fields
-
template uuid · required Template to update. -
labels array · required The complete set of labels: name, title (`description`), help text (`help_text`, up to 500 characters, shown under the field), field type, page (from 0), coordinates (`x`, `y`, `width`, `height` as fractions of the page, 0–1, from the top-left corner), `form_order` (place in the fill order of its section, from 1; 0 follows the page) and, for text fields only, a `format` (`email`, `phone`, `codice_fiscale`, `partita_iva`, `iban`, `cap`) or `options`, the list the value is picked from (up to 50 options of at most 100 characters, no duplicates). Labels left out are deleted. -
groups array · optional Sections of the form: `name`, `title`, `description`, `order`, `before_ungrouped` (filled in before the fields without a section), an optional checkbox condition (`show_if`, `show_if_checked`) and `min_checked` / `max_checked`, how many of its checkboxes may be ticked together (0 = no limit: `max_checked` 1 makes a single choice). Left out, the current sections are deleted. -
signers array · optional Who signs the documents of the template: `name`, `title`, `order` (the signing order). Each signature field then names its signer in `signer_role`. Left out, the current signers are deleted. -
attachments array · optional The attachments to ask for: `name`, `title`, `description`, `required`, `formats` (`pdf`, `jpg`, `png`, `heic`), `max_size_mb` (up to 10), `signer_role` (the signer who uploads it) and `order`. Left out, the current ones stay. -
consents array · optional Consents asked outside the PDF: `name`, `text` (up to 1000 characters), `required`, `document` (id of a document uploaded with `consent-documents`), `signer_role` (blank = every signer) and `order`. Left out, the current ones stay.
Required attachments, or ones assigned to a signer, need Pro or above; how many attachments each signer can be asked for depends on the plan (3 on Starter, 10 from Pro).
POST /templates/{id}/consent-documents/ multipart/form-data
Upload the PDF a consent points at, such as the general terms.
Fields
-
file file · required PDF up to 10 MB.
The document never changes: a new version is a new document, with its own fingerprint. Returns `id`, `file_name`, `size`, `sha256`.
Compilations
Compilations — one rendered PDF
Submit a values map for a template and get the finished PDF back in the same request: the response carries `status: done` and a signed `download_url`. If the engine fails for another reason, `status` is `failed` with an `error` message, and no quota is used.
POST /compilations/ application/json
Create a compilation and render it.
Fields
-
template uuid · required Template to render. -
name string · optional Human-readable name, up to 200 characters. -
values object · required Map of `label_name → value`. Accepts strings, numbers, booleans and null — coerced server-side per field type. A field with a `format` or `options` accepts only a value of that format or from the list, otherwise `400 invalid` with the error under its name in `extra.fields`; codice fiscale, partita IVA and IBAN are stored without spaces and in capitals. -
store_values bool · default true When false, the values are wiped once the PDF is produced.
Request body
{
"template": "3f0e4b2a-21c2-4d34-9c8d-9b0a5d6b3f11",
"name": "NDA — Acme Corp",
"values": {
"full_name": "Jordan Rivera",
"effective_date": "2026-06-01",
"agrees_to_terms": true
}
}
Response
HTTP/1.1 201 Created
{
"id": "c2a1...e9",
"name": "NDA — Acme Corp",
"template": "3f0e4b2a-21c2-4d34-9c8d-9b0a5d6b3f11",
"values": { "full_name": "Jordan Rivera", "effective_date": "2026-06-01", "agrees_to_terms": true },
"store_values": true,
"status": "done",
"download_url": "https://…",
"output_sha256": "9b1c…4e",
"is_signed": false,
"signature_request": null,
"attachments_count": 0,
"consents_count": 0,
"error": "",
"expires_at": null,
"created_at": "2026-09-30T10:13:51Z",
"updated_at": "2026-09-30T10:13:52Z"
}
Statuses: `done`, `failed`, `pending` (only while a share-link submission is being rendered) and `expired` once the retention period has deleted the PDF. `download_url` is a pre-signed link valid for up to 30 minutes: fetch a fresh one with `GET` when you need it. `GET` also fills in `expires_at`, when the retention deletes the PDF (`null` if it never does). `signature_request` is `{ id, status, signed, total }` for a document sent to several signers. Values that can't be printed in their field return `400` with code `unprintable` or `does_not_fit`.
GET /compilations/
List compilations.
Filters: `?status=`, `?template=`, `?search=` (name), `?expiring=1` (PDFs the retention deletes within 14 days).
GET /compilations/{id}/
Retrieve a compilation, with a fresh `download_url`.
PATCH /compilations/{id}/
Re-render with new values, or rename the compilation.
Sending `values` re-renders inside the request, returns `200` and uses one PDF of quota. Sending only `name` is free. `store_values` can go with either; left out, the current choice stays. A signed compilation can't be changed (`409 signed`), nor can one with consents (`409 has_consents`).
POST /compilations/preview/ application/json
Render a PNG preview of one page with your values, without using quota.
Fields
-
template uuid · required Template to preview. -
page int · required Page number, starting from 0. -
values object · required Values to draw, even partial. -
dpi int 60–220 · default 110 Resolution of the image.
DELETE /compilations/{id}/
Permanently delete the compilation, its rendered PDF and its attachments.
A signed compilation, or one with consents, can't be deleted with an API key: `403 admin_required`. An owner or administrator deletes it from the dashboard, confirming with `confirm=ELIMINA` (otherwise `400 confirmation_required`). For a signed document the fingerprints, the issuer's name and the dates are kept, so the verification page reports it as signed and then deleted.
Batches
Batches — thousands of PDFs in one call
Two entry points: `/batches/json/` for rows in the request body, `/batches/upload/` for a CSV or XLSX file. The batch runs in the background: the call returns `202` right away, and at the end you get a single `result.zip` with one PDF per row (rows that fail appear as `.error.txt` files). Batches need a plan with bulk processing (Pro and above).
POST /batches/json/ application/json
Submit up to 5,000 rows of values inline.
Fields
-
template uuid · required Template to render. -
rows array · required 1–5000 objects `label_name → value`. A `__filename` key names each PDF in the zip. -
completion_webhook_endpoint uuid · optional Saved endpoint to notify on completion. -
completion_webhook_url url · optional One-off webhook URL. The response carries its `completion_webhook_secret`, shown only once. -
store_source bool · default true When false, the source rows are deleted once the batch is done; the zip stays.
Response
HTTP/1.1 202 Accepted
{
"id": "b1f2...77",
"source": "json",
"status": "pending",
"total": 0,
"expected_total": 2,
"succeeded": 0,
"failed": 0
}
Webhook precedence: saved endpoint → inline URL → org default endpoint. If none is configured, the batch runs without notification.
POST /batches/upload/ multipart/form-data
Submit a CSV or XLSX file (up to 20 MB).
Fields
-
template uuid · required Template to render. -
file file · required .csv or .xlsx, ≤ 20 MB. -
sheet string · optional XLSX sheet to read. -
column_mapping JSON string · optional Map `label_name → column name`. Columns named like a label are matched automatically. -
store_source bool · default true When false, the uploaded file is deleted once the batch is done; the zip stays.
GET /batches/{id}/
Poll a batch.
Statuses: `pending → running → done | failed`, then `expired` after retention. When `done`, `succeeded + failed` equal `expected_total` and `download_url` points at the zip. A batch that fails as a whole ends as `failed`, with no zip.
Share requests
Share requests — let the recipient fill it themselves
Generate a token-protected link to a hosted form. The recipient opens it on any device, optionally verifies their email with a code, fills the form and submits. If the template asks for attachments, they upload them from the same form. If it has signature fields, they review the rendered PDF and sign it before submitting. A link reaches one person: a template with more than one signer is refused (`400 several_signers`), send it with a signature request. Through the API, share links need Pro or above; in the dashboard, single links are included from Starter.
POST /share-requests/ application/json
Create a single share request.
Fields
-
template uuid · required Template to expose. -
recipient_email email · optional If set, we deliver the link by email. Required with `verify_email` and when the template asks a required consent (`400 recipient_required`). -
verify_email bool · default false Gate submission behind an email code. Always on when the template asks a required consent. -
require_review bool · default false Show the rendered PDF for review before submitting. Always on when the template has signature fields. -
signature_otp bool · default true Confirm the signature with a code sent by email. -
send_copy_to_recipient bool · default false Email the final PDF to the recipient; on a link without a recipient, to the address verified with the signing code. -
notify_sender_on_complete bool · default false Email you when the recipient submits. -
prefill_values object · optional Values you already know. -
expires_in_days int 1–90 · default 7 Link lifetime. -
invitation_language string · optional Language of the invitation email, e.g. `en` or `it`. -
reminders bool · default false Automatic reminders to the recipient. Needs `recipient_email`: without one, `reminders_enabled` comes back false.
Automatic reminders (`reminders: true`): the same link, same token, at most twice per recipient. The first goes 3 days after the invitation, if more than 24 hours are left before the expiry; the last within the 24 hours before the expiry, skipped if that person received a message in the previous 24 hours. They go out only Monday to Saturday, 09:00–18:59 Italian time, and never once the link is completed, revoked or expired. Every link carries `reminders_enabled`, `reminder_count` (automatic reminders sent), `last_reminded_at` (the latest message after the invitation, automatic or resent by you), `resend_count` and `first_opened_at`, the first time the recipient opened the link.
POST /share-requests/bulk/ application/json
Create a batch of links from a list of recipients.
Fields
-
template uuid · required Shared template. -
name string · required Batch name, 2–200 characters. -
requests array · required One object per recipient: `email` (required), `verify_email`, `send_copy`, `notify_sender`, `prefill`. -
expires_in_days int 1–90 · default 7 Applied to every link in the batch. -
reminders bool · default false Automatic reminders to every recipient of the batch. -
invitation_language string · optional Language of all the invitations.
Rows with an invalid email are skipped and counted in `failed`.
POST /share-requests/bulk-upload/ multipart/form-data
Create a batch of links from a file.
Fields
-
template uuid · required Shared template. -
name string · required Batch name. -
file file · required .csv, .xlsx or .json, up to 20 MB and 5,000 rows. Columns: `email`, optional `verify_email` and `send_copy`; any column named like a label prefills it. -
expires_in_days int 1–90 · default 7 Applied to every link. -
reminders bool · default false Automatic reminders to every recipient: `true` or `1` turns them on.
GET /share-requests/
List share requests.
Filters: `?status=pending|completed|expired|revoked`, `?batch=`, `?q=` (recipient email). Without `?batch=` only the links created one by one come back; the links of a batch with `?batch=<id>`. Completed links carry the `compilation` and its `compilation_download_url`. To know what to follow up: `?expiring_within=N` returns the links still open that expire within N days, `?expired_within=N` those that expired unanswered in the last N days (completed and revoked ones excluded); N from 1 to 90, otherwise `400`. Without `?batch=` these two include the links of batches too.
GET /share-requests/batches/
List the batches of links.
Filters: `?q=` (batch or template name), `?status=open|done`. Each batch carries `id`, `name`, `source`, `template`, `template_name`, `total`, `sent`, `completed`, `failed` and `created_at`; `GET /share-requests/batches/{id}/` reads one, with `open_links` (links still open), `open_with_recipient` (those of them with a recipient) and `remindable_links` (those a reminder reaches right now), which the list leaves `null`.
POST /share-requests/batches/{id}/remind/
Send the invitation again to every recipient of a batch still to answer.
No body. Every open link with a recipient gets the same link again, under the rules of `resend`: once every 24 hours (automatic reminders count too) and 5 times at most per link. Returns `200` with `{"sent": n, "skipped": n, "reasons": {"no_recipient": n, "cooldown": n, "limit": n}}`. Completed, revoked and expired links are left alone. Same permissions as the other batch endpoints.
POST /share-requests/batches/{id}/revoke-pending/
Revoke every link of a batch still open.
No body. Returns `200` with `{"revoked": n}`; their codes and signing drafts stop working with them. Completed, expired or already revoked links stay as they are.
GET /share-requests/batches/{id}/export/
Download the links of a batch as CSV.
UTF-8 with BOM and `;` as separator, so Excel opens it in columns; dates in Italian time. Columns: recipient (who signed, when known), email, status, invitation sent, first opened, completed, expires, reminders sent (automatic and by hand). Cells starting with `=`, `+`, `-`, `@`, a tab or a carriage return get a leading `'`, so no spreadsheet runs them as formulas. Headers follow `Accept-Language`.
POST /share-requests/{id}/revoke/
Revoke a link: it stops working immediately.
POST /share-requests/{id}/recreate/
Issue a fresh link with the same settings and send a new invitation.
POST /share-requests/{id}/resend/
Send the invitation again, with the same link.
No body. Returns `200` with the link. `409 not_pending` when the link is no longer open, `409 no_recipient` when it has no recipient. `429 resend_cooldown` within 24 hours of the latest message to that person (reminder or resend), with `Retry-After`, or after 5 resends in all.
POST /share-requests/{id}/extend/ application/json
Extend the expiry of an open link.
Fields
-
days int 1–90 · required Days added to the current expiry.
Returns `200` with the updated link; `400` when `days` is not valid. `409 not_open` when the link is completed, revoked or expired: an expired one is issued again with `recreate`. After an extension the last reminder can go again before the new expiry. Same permissions as revoke.
Signing
Signing — get a document signed through the API
A signing draft is the filled PDF, frozen: the signer reviews exactly those bytes and the signatures are stamped on them. The signature is completed on the signer's own device, through a link you send, or collected by your integration and sent with your API key. Either way the signed PDF ends with an audit page, and the record names the API key used. Simple electronic signature (SES) under art. 3 of eIDAS.
POST /signing-drafts/ application/json
Fill and freeze the document to sign.
Fields
-
template uuid · required A template with signature fields. -
values object · required Map of `label_name → value`, as for a compilation. -
name string · optional Name of the resulting compilation. -
compilation uuid · optional Sign an existing compilation that isn't signed yet. -
store_values bool · default true When false, the values are wiped once signed.
The response carries `id`, `sha256`, `pdf_url` and `pages` to show the document, `signature_labels` and `consent_text`, the declaration the signer accepts. The draft lasts 24 hours.
POST /signing-drafts/{id}/handoff/
Get a link for the signer's own device.
Returns `{ url, expires_at }`: a link valid for 30 minutes. The signer opens it, reviews the document and signs there; the record shows the `handoff` method with your API key.
GET /signing-drafts/{id}/
Check whether the draft has been signed.
Returns `{ id, finalized, compilation }`. Or listen for `compilation.signed`.
POST /signing-drafts/{id}/finalize/ application/json
Send a signature collected by your integration.
Fields
-
document_sha256 string · required The draft's `sha256`: refused if the document changed. -
signer_name string · required Full name of the signer. -
consent bool · required The signer accepted `consent_text`. -
signatures object · required Map of `signature field → PNG data URL`, up to 512 KB each.
Request body
{
"document_sha256": "<sha256>",
"signer_name": "Mario Rossi",
"consent": true,
"signatures": { "signature": "data:image/png;base64,iVBORw0KGgo..." }
}
Returns the signed compilation. The record shows the `api` method with the name of your API key, and so does the audit page.
GET /compilations/{id}/signatures/
Read the signature records of a compilation.
Method (`remote`, `in_person`, `handoff`, `api`), signer, email and code verification, IP, device, `api_key_label` and the SHA-256 fingerprints of the document and of each signature.
Signature requests
Signature requests — several signers, one document
A signature request has a document signed by several people, each on their own signature fields. The signers are defined on the template, in signing order (the Signers panel of the editor, or `signers` in `bulk-upsert`), and each signature field names who signs it. Every signer gets a personal link by email, reviews the document, confirms with a code and signs. In sequence, each signer is invited once the previous one has signed and sees the signatures already there; in parallel, everyone is invited at once. If the template assigns attachments to a signer, they upload them from their link before signing. The last signature closes the PDF with an audit page listing every signer. Pro and above.
POST /signature-requests/ application/json
Create a request and invite who signs first.
Fields
-
template uuid · required A template with signature fields. -
signers array · required One `{ role, name, email }` per signer of the template, where `role` is the signer's `name`. A template without signers takes a single signer with `role` empty. -
order sequential | parallel · default sequential One signer at a time, or everyone at once. -
fill_mode sender | first_signer · default sender Who fills in the document: you with `values`, or the first signer from their link. -
values object · optional The document data; with `first_signer`, what the first signer finds already filled in. -
signature_otp bool · default true Every signer confirms with a code sent by email. -
notify_sender_on_complete bool · default false Email you the signed PDF when everyone has signed. -
send_copy_to_signers bool · default false Email every signer their copy at the end. -
expires_in_days int 1–90 · default 14 Lifetime of the links. -
invitation_language string · optional Language of the invitations, e.g. `en` or `it`. -
reminders bool · default false Automatic reminders to the signer whose turn it is.
Request body
{
"template": "3f0e4b2a-21c2-4d34-9c8d-9b0a5d6b3f11",
"order": "sequential",
"fill_mode": "sender",
"values": { "full_name": "Jordan Rivera" },
"signers": [
{ "role": "client", "name": "Jordan Rivera", "email": "[email protected]" },
{ "role": "guarantor", "name": "Sasha Park", "email": "[email protected]" }
]
}
Returns `201` with every signer's `status` (`waiting`, `invited`, `signed`) and their personal `url` while they can sign. With `sender` the document is rendered and one PDF of quota used now; with `first_signer`, when the first signer submits. Creating the request also counts one signed document of your plan, given back if the request closes with no signature. With `reminders: true` the reminders follow the rules of share links and, in sequence, go only to the signer whose turn it is; in parallel, to everyone still to sign. The request carries `reminders_enabled`; every signer `reminder_count`, `last_reminded_at` and `first_opened_at`.
GET /signature-requests/
List signature requests.
Filters: `?status=open|completed|expired|revoked`, `?template=`, `?q=` (signer email), `?expiring_within=N` (open requests expiring within N days), `?expired_within=N` (expired unsigned in the last N days); N from 1 to 90.
GET /signature-requests/{id}/
Where the request stands: signers, compilation and, once completed, `download_url`.
POST /signature-requests/{id}/revoke/
Close the request: the links stop working and the compilation is marked failed.
POST /signature-requests/{id}/signers/{signer_id}/resend/
Send the invitation again to a signer whose turn it is (at most once a minute).
POST /signature-requests/{id}/extend/ application/json
Extend the expiry of an open request.
Fields
-
days int 1–90 · required Days added to the current expiry.
Returns `200` with the updated request; `400` when `days` is not valid, `409 not_open` when it is completed, revoked or expired. After an extension the last reminder can go again before the new expiry. Same permissions as revoke.
Attachments
Attachments — the documents the customer uploads
The attachments to ask for are defined on the template (`attachments` in `bulk-upsert`). The customer uploads them from the link or the QR code, and they stay tied to the compilation, the share link or the signature request. They live in private storage: they never have a public URL, can only be downloaded with your API key, always as a file to save and never as a preview, and are deleted together with the PDFs, on your plan's retention. The file type is checked from its bytes, not its extension.
GET /attachments/
List your organisation's attachments.
Filters: `?compilation=`, `?share_request=`, `?signature_request=`. Each attachment carries `id`, `requirement`, `title`, `signer_role`, `file_name`, `content_type`, `size`, `sha256`, `uploaded_at` and `download_url`.
GET /compilations/{id}/attachments/
The attachments of a compilation.
GET /attachments/{id}/download/
Download the file of an attachment.
This is the attachments' `download_url`: it needs your API key and returns the file as a download, under its file name.
GET /compilations/{id}/package/
Download the complete file: one ZIP with the PDF and its attachments.
Consents
Consents — what the customer accepted
Consents are defined on the template (`consents` in `bulk-upsert`). Whoever fills in or signs ticks them on the link page, on the phone or when signing in the app; boxes start empty and every answer is recorded with the exact text, the linked document fingerprint, the outcome, time, IP and browser. A required consent not accepted blocks submitting and signing; one never ticked counts as refused. When the template asks a required consent, links require email verification and signature requests the emailed code. A compilation with consents can no longer be changed (`409 has_consents`).
GET /consents/
List the consent answers of your organisation.
Filters: `?compilation=`, `?share_request=`, `?signature_request=`. Each answer carries `requirement`, `text`, `required`, `signer_role`, `accepted`, `answered_at`, `document_name`, `document_sha256`, `ip` and `user_agent`.
GET /compilations/{id}/consents/
The consent answers of a compilation.
Webhooks
Webhooks — events delivered to your endpoint
Register an HTTPS endpoint and DataPDFEngine signs every request with HMAC-SHA256, using the endpoint's secret. `batch.completed` goes to the endpoint chosen for the batch, or to the default one; the other events go to the organisation's default endpoint. Each request carries a unique delivery ID, so your handler can process it only once. Signing and completion events carry an `attachments` array: `{ id, requirement, title, signer_role, file_name, content_type, size, sha256, uploaded_at, download_url }`, where `download_url` needs your API key. They also carry a `consents` array: `{ requirement, text, required, signer_role, accepted, answered_at, document_name, document_sha256, ip, user_agent }`. Webhooks need Pro or above.
Signature verification (HMAC-SHA256)
Each request carries these headers. Verify the signature before trusting the payload, and always compute the HMAC over the raw request body, before any JSON parsing.
-
X-DataPdfEngine-Event Event name, e.g. `batch.completed`. -
X-DataPdfEngine-Delivery Unique ID of the delivery: store it to ignore duplicates. -
X-DataPdfEngine-Timestamp Unix time, in seconds, when the request was signed. -
X-DataPdfEngine-Signature `t=<unix>,v1=<hex>`: `<hex>` is the HMAC-SHA256 of `"<unix>." + raw body`, keyed with your endpoint secret. Reject timestamps more than 5 minutes away from your clock.
import crypto from "node:crypto";
// header: "t=<unix>,v1=<hex>"
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
(header ?? "").split(",").map((part) => part.split("=", 2))
);
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(parts.v1, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
POST /webhooks/endpoints/
Register a new endpoint. The response includes the `secret` once — store it.
PATCH /webhooks/endpoints/{id}/
Update an endpoint, disable it (`is_active`) or make it the default for batches (`is_default`).
POST /webhooks/endpoints/{id}/rotate-secret/
Issue a new secret, returned once. The old one stops working immediately.
POST /webhooks/endpoints/{id}/test/
Send a `test.ping` and return the target's answer: `{ ok, response_status, delivery }`.
GET /webhooks/deliveries/
Inspect recent deliveries: status code, attempt count, response body.
Network errors, `5xx` and `429` are retried, up to 6 attempts, waiting 30 s and doubling each time. Redirects aren't followed and private addresses are refused.
Events
-
batch.completed Sent when a batch finishes as `done`. Payload: `{ event, batch_id, organization_id, template_id, totals: { total, succeeded, failed }, download_url }`. -
compilation.signed Sent for every signature collected in person, through the API, on the signer's device (`handoff`) or through a share link. The signers of a signature request trigger `signature_request.signed` instead. Payload: `{ event, compilation_id, organization_id, template_id, share_request_id, method, signer: { name, email, email_verified }, api_key, signed_at, document_sha256, signed_document_sha256, download_url, attachments, consents }`. -
share_request.completed Sent when a share link has been filled in and its PDF is ready (signed, if it had signature fields). Payload: `{ event, share_request_id, share_batch_id, organization_id, template_id, compilation_id, recipient_email, completed_at, signed, download_url, attachments, consents }`. -
signature_request.signed Sent when a signer of a request has signed. Payload: `{ event, signature_request_id, organization_id, template_id, compilation_id, signer: { id, role, role_title, name, email, position, signed_at }, email_verified, document_sha256, remaining, attachments, consents }`. -
signature_request.completed Sent when everyone has signed. Payload: `{ event, signature_request_id, organization_id, template_id, compilation_id, completed_at, signers, signed_document_sha256, download_url, attachments, consents }`, where each of `signers` has the same fields as `signer` above. -
test.ping Sent when you test an endpoint, from the dashboard or with `POST /webhooks/endpoints/{id}/test/`. Payload: `{ event, endpoint_id, organization_id, sent_at }`. Use it to check signature handling and reachability before going live.
Error model
Errors come back as JSON: `{ code, detail, extra? }`. `code` is stable, `detail` is a readable message, and `extra` carries details such as the invalid fields or the missing feature.
| HTTP | Code | Detail |
| 400 | invalid | Payload didn't validate. `detail` names the first problem; `extra.fields` lists them all. |
| 400 | unprintable · does_not_fit | A value can't be printed in its field, or doesn't fit. Details in `extra.fields.values`. |
| 400 | target_not_allowed | The webhook URL points at a private address. |
| 400 | document_changed · signature_missing · signature_blank | Signing refused: the document changed after the review, a required signature is missing or empty. |
| 400 | signer_name_required · consent_required · signature_invalid · signature_too_large | Signing refused: the signer's name is missing, `consent` is not true, or a signature is not a PNG data URL or is over 512 KB or 2048 pixels a side. |
| 400 | draft_unusable | The signing draft expired or was already used: create a new one. |
| 400 | no_signature_fields | The template has no signature fields. |
| 400 | several_signers | The template has more than one signer: send it with a signature request, not a share link. |
| 400 | recipient_required · invalid_email | A valid email address is needed: for a share link with `verify_email` or a required consent, or for each signer of a request. |
| 400 | confirmation_required | Deleting a signed compilation, or one with consents, needs `confirm=ELIMINA`. |
| 400 | pdf_unreadable · pdf_encrypted · pdf_damaged · pdf_empty · pdf_too_many_pages · pdf_page_too_large | The uploaded PDF can't become a template: unreadable, password-protected, damaged, with no pages, over 200 pages or with pages too large. |
| 401 | authentication_failed | Invalid or revoked API key. |
| 401 | not_authenticated | Missing `Authorization` header, or a scheme other than `Bearer`. |
| 403 | feature_required | Your plan doesn't include this feature; `extra.feature` names it. |
| 403 | admin_required | A signed compilation, or one with consents, can only be deleted by an owner or administrator from the dashboard, not with an API key. |
| 404 | not_found | Resource doesn't exist, or belongs to another organisation. |
| 409 | signed | The compilation is signed and can't be changed. |
| 409 | already_signed | The compilation is already signed. |
| 409 | signing_in_progress · not_your_turn | The document is waiting for signatures and cannot be changed, or a signer's turn has not come yet. |
| 409 | has_consents | The compilation has recorded consents and can't be changed (`400` from `POST /signing-drafts/` with that `compilation`). |
| 409 | already_submitted | The document has already been submitted. |
| 409 | template_in_use | The template is still used by compilations, batches or links. |
| 409 | endpoint_inactive | The webhook endpoint is disabled. |
| 429 | quota_exceeded | Monthly quota used up on a plan without overage, or your own overage cap reached. `extra.limit` holds the quota. |
| 429 | throttled | Too many requests (240 per minute per organisation), or 20 invalid API keys in a minute from your IP address, which blocks valid keys from it too until the minute is over. Retry after `Retry-After` seconds. |
| 429 | invite_cooldown | An invitation was resent less than a minute ago. Retry after `Retry-After` seconds. |
| 500 | server_error | Unexpected error on our side. |
| 502 | delivery_failed | Only from the endpoint test: the target couldn't be reached. |