Dazr Sign API: developer docs

Quick start

  1. Enable the Dazr Sign API. In the Sign portal, open Developers and choose Enable Dazr Sign API for your app. Every app of your organisation is listed there and in the Dazr Identity console, with the same client ID and secret; each API is enabled on its own. You can also register a new app there.
  2. Ask for the sign scope. Send people through the usual Sign in with Dazr Identity flow with scope=openid sign (add offline_access for a refresh token). The consent screen says your app may prepare and send documents for signature in their name, and see their status. This works with the Dazr Sign API alone; asking for people’s data too (profile, email and the others) needs the Dazr Identity API enabled as well.
  3. Call the API with the access token. The base address is https://sign.dazr.eu/api/sign/v1. Send Authorization: Bearer <access token> and JSON.
  4. Create and send a document. Upload a PDF with text tags and a list of recipients, then send it, or open the prepare popup so the person places the fields.
// Node 18+: create a document from a PDF with text tags, then send it
import fs from 'node:fs';
const api = 'https://sign.dazr.eu/api/sign/v1';
const headers = { Authorization: 'Bearer ' + accessToken, 'Content-Type': 'application/json' };

let res = await fetch(api + '/documents', {
  method: 'POST',
  headers: { ...headers, 'Idempotency-Key': 'order-1042' },
  body: JSON.stringify({
    title: 'Service agreement',
    file: fs.readFileSync('agreement.pdf').toString('base64'),
    recipients: [
      { name: 'Alex Example', email: 'alex@example.eu' },
      { name: 'Sam Client', email: 'sam@client.example' },
    ],
  }),
});
const { document } = await res.json();

res = await fetch(api + '/documents/' + document.id + '/send', { method: 'POST', headers });
console.log((await res.json()).document.status); // "sent"

Text tags

Type the fields into the PDF as text. Each tag is replaced by a field of the same size or larger, and the tag text is painted over, so signers never see it.

TagField
{{signature:1}}Signature of the first recipient
{{initials:2}}Initials of the second recipient
{{name:1}} {{date:1}}Name and the date of signing, filled in by Dazr Sign
{{text:2:Company}}A text field with the label Company
{{checkbox:2:Terms:optional}}An optional checkbox
{{company:1}}Company name (filled in when the person signs on behalf of an organisation)

The number is the recipient’s position in recipients, starting at 1. A tag for a recipient you did not list adds an empty recipient, to fill in in the prepare popup. Send "text_tags": false to ignore tags. You can also send fields with positions in points (1/72 inch) from the top-left corner of the page: { "recipient": 1, "type": "signature", "page": 1, "x": 72, "y": 600, "width": 180, "height": 48 }. A value on a text, company or checkbox field fixes it: the signer sees it and cannot change it.

Create a draft with "prepare": { "origin": "https://app.example.eu" } and you get a prepare_url. Open it in a popup: the person places recipients and fields in a compact editor, then clicks Send or Save as template. For a signer whose turn it is, POST /documents/{id}/recipients/{recipient_id}/signing_url gives a signing popup. Both links are valid for 10 to 15 minutes and work once. The person signs in with Dazr Identity inside the popup if needed, as the address the document was sent to.

When it is done, the popup sends { type: 'dazr-sign', event, document_id, template_id, recipient_id, state } with postMessage to the page that opened it, only if that page is on one of your app’s popup origins (by default, the origins of your redirect URIs), and closes. event is sent, saved, cancelled, signed or declined. With redirect_uri (one of your registered redirect URIs) and state, a popup that has lost its opener goes back there with the same values as query parameters.

<script src="https://sign.dazr.eu/sdk.js"></script>
<script>
document.querySelector('#sign').addEventListener('click', async () => {
  // Ask your server for the URL after the click: the popup opens at once.
  const url = fetch('/my-server/prepare-url').then(r => r.json()).then(j => j.prepare_url);
  const result = await DazrSign.open(url);
  if (result.event === 'sent') showSent(result.document_id);
  // 'closed': the window was closed; check the status with the API or a webhook
});
</script>

If your page sends Cross-Origin-Opener-Policy: same-origin, use same-origin-allow-popups instead, or the popup cannot report back.

Qualified signatures

Each signer has a level: "advanced" (the default: the email address is confirmed by signing in with Dazr Identity) or "qualified". A qualified signer downloads the PDF in Dazr Sign, signs it with their own qualified electronic signature (an electronic ID card, a signing app such as itsme, a qualified certificate in Adobe Acrobat Reader) and uploads the signed file. Dazr Sign checks it against the EU trusted lists and accepts it only if it is a qualified signature on exactly the file it handed out. Dazr does not issue qualified signatures.

Set the level per recipient: { "name": "Mario Rossi", "email": "mario@example.eu", "level": "qualified" }. A qualified signer has no initials fields. name_rule on the document says what happens when the name on the certificate differs from the recipient’s name: "warn" (the default: accepted, with name_matches false) or "block" (refused). Templates keep the level of each role, and from_template can raise a role to "qualified".

Every signature stays valid: later signatures and the final Dazr seal are added as incremental updates, and the audit trail of such a document is a separate sealed PDF at GET /documents/{id}/audit.pdf. Recipients in documents and webhooks have level and, once a qualified signer has signed, certificate: subject (common_name, given_name, surname, serial_number, country, organization, organization_identifier), issuer, serial, sha256, trust_service_provider, trusted_list_country, signing_time, trusted_timestamp, format (PAdES or CAdES) and name_matches. Any other level gives 400 invalid_level.

Webhooks

Set a webhook URL for your app in the Sign portal under Developers. You get its signing secret once. Dazr Sign sends sign.document.sent, viewed, signed, completed, declined, voided and expired for the documents your app created, with the document’s status and its recipients’ status, never its contents. Failed deliveries are retried after 5 minutes, 30 minutes, 2, 6, 12 and 24 hours; the delivery log shows every attempt and can resend one.

// Node: check the Dazr-Signature header (t=<unix time>,v1=<HMAC-SHA256 hex>)
import crypto from 'node:crypto';

export function verify(rawBody, header, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(m[1] + '.' + rawBody).digest();
  return crypto.timingSafeEqual(want, Buffer.from(m[2], 'hex'));
}
// The body: { id, type: 'sign.document.completed', created, data: { document: { id, status, recipients: [...] } } }

Reference

All paths start with https://sign.dazr.eu/api/sign/v1. Times are ISO 8601 in UTC.

RequestWhat it does
POST /documentsCreates a draft from a PDF: file in base64, a multipart upload (file plus data as JSON), or an upload_id. Optional: title, message, recipients, fields, signing_order, deadline_days, reminder_days, send, prepare.
POST /documents/from_templateCreates a draft from a template: template_id, recipients as { role, name, email }, values by field ID or label.
GET /documentsLists your app’s documents for this person, newest first: limit (up to 100), cursor, status.
GET /documents/{id}The document with its status, recipients and fields.
POST /documents/{id}/sendSends a draft.
POST /documents/{id}/remindReminds the signers whose turn it is (once an hour at most).
POST /documents/{id}/voidCancels a document out for signature, with an optional reason.
DELETE /documents/{id}Deletes a draft or a closed document.
POST /documents/{id}/prepare_urlA new prepare popup for a draft: origin, redirect_uri, state.
POST /documents/{id}/recipients/{recipient_id}/signing_urlA signing popup for a signer whose turn it is: origin, redirect_uri, state.
GET /documents/{id}/original.pdfThe PDF as it was sent.
GET /documents/{id}/signed.pdfThe sealed copy, once everyone has signed.
GET /documents/{id}/audit.jsonThe audit trail: events, hashes and signers.
GET /documents/{id}/audit.pdfThe separate, sealed audit trail of a document with qualified signers, once everyone has signed.
POST /uploadsStarts an upload in parts for PDFs over 4 MB: size. Then PUT /uploads/{upload_id}/parts/{n} with the raw bytes of each part.
GET /templatesThe person’s templates, with their roles and fields.
GET /templates/{id}One template.
POST /templatesSaves one of your app’s documents as a template: document_id, name.
DELETE /templates/{id}Deletes a template.

Send an Idempotency-Key header when you create a document: the same key and body within 24 hours return the same document (header Idempotent-Replayed: true) instead of a second one.

Errors and limits

Errors have an HTTP status and a body like { "error": { "code": "not_found", "message": "..." } }, sometimes with details such as index.

StatusCodes
400invalid_field, invalid_recipients, invalid_email, not_pdf, pdf_unreadable, pdf_encrypted, invalid_origin, invalid_redirect_uri, unknown_role, unknown_field
401unauthenticated, invalid_token (expired or revoked, or the person removed your app)
403insufficient_scope, sign_api_disabled
404not_found (also for documents your app did not create), template_not_found
409not_draft, not_sent, not_your_turn, not_signable, signer_without_fields, no_signers, void_first, idempotency_in_progress
413pdf_too_large, too_large, quota
422idempotency_key_reused
429rate_limited, with a Retry-After header

Transferring an app

An app can move to another organisation registered on Dazr, under Transfer app in the app’s settings. With the Dazr Sign API enabled, it can only move to a verified organisation, and an owner or admin there must accept within 14 days. The Dazr Identity developer docs describe every step.