ENEnglish
Sealique · API v1

Signatures from your own software

One request sends a PDF for signature. Webhooks tell you when it is opened, signed and sealed. Every sealed file can be verified by anyone, with or without a key.

Endpoint reference openapi.json Read the guide

Quickstart

  1. Create an API key under Integrations. Start with a test key: it behaves exactly like a live one but sends no e-mail.
  2. POST a PDF and the signers. The response carries one secure signing link per signer.
  3. Register a webhook (or poll the status endpoint) and download the sealed PDF and its certificate when the document is finalized.
curl -X POST https://sealique.com/api/v1/documents \
  -H "Authorization: Bearer af_test_…" \
  -H "Idempotency-Key: order-1042" \
  -F "file=@contract.pdf" \
  -F "title=Service agreement" \
  -F 'signers=[{"name":"Aline Uwase","email":"aline@example.com","title":"CEO"},
               {"name":"Eric Mugisha","email":"eric@example.com"}]' \
  -F "signing_order=sequential" \
  -F "return_url=https://app.example.com/signed"

Prefer JSON? Send Content-Type: application/json with file_base64 instead of the multipart file; every other option is the same.

{
  "id": "6f1c…", "status": "sent", "mode": "test", "title": "Service agreement",
  "signing_order": "sequential", "auto_finalize": true,
  "signers": [{"id": "…", "name": "Aline Uwase", "email": "aline@example.com", "status": "invited",
               "signing_url": "https://sealique.com/sign/…", "link_expires_at": "2026-11-08T10:00:00+00:00"}],
  "signing_links": [{"id": "…", "name": "Aline Uwase", "email": "aline@example.com",
                     "signing_url": "https://sealique.com/sign/…", "expires_at": "…"}],
  "verification_id": null, "verify_url": null, "document_hash": null, "finalized_at": null,
  "certificate_url": null, "file_url": null, "metadata": {}, "events": [ … ]
}

Authentication, modes and scopes

Every key-authenticated request carries Authorization: Bearer <key>. Keys are shown once, at creation, and can be revoked at any time.

Modes

  • af_live_… — real documents, invitations are e-mailed, webhooks go to live endpoints.
  • af_test_… — sandbox: documents are flagged, nobody is e-mailed (you receive the links in the response), webhooks go to test endpoints only. The signing pages and sealing work exactly as in live mode.

Every response echoes "mode": "live" | "test". A key only sees the documents created in its own mode.

Scopes

  • documents:read
  • documents:write
  • verify
  • webhooks

A key without explicit scopes has all of them. A call outside the key's scopes gets 403 insufficient_scope. Keys may carry an expiry date; expired keys get 401 unauthorized.

Document lifecycle

Side exits: declined when a signer refuses, canceled when you call cancel. With auto_finalize (the default) the step from completed to finalized happens by itself; otherwise call POST …/finalize.

Sealing stamps every signature into the PDF, applies the platform certification signature and one PAdES signature per signer, then hashes the file (SHA-256) and issues a verification ID printed in every signature block. The certificate of completion is a separate PDF.

MethodPathPurposeScope
POST/api/v1/documentsCreate (multipart or JSON file_base64). Options: title, signers[{name,email,title,order}], signing_order, auto_finalize (default true), message, send (default true), return_url, require_otp, metadata, suppress_email.documents:write
GET/api/v1/documents?status=&cursor=&limit=List your documents, newest first, cursor-paginated.documents:read
GET/api/v1/documents/{id}Status, signers, links, verification_id, verify_url, document_hash, finalized_at, certificate_url, file_url, events, metadata.documents:read
POST/api/v1/documents/{id}/signersAdd a signer (draft or sent).documents:write
DELETE/api/v1/documents/{id}/signers/{sid}Remove a signer who has not signed.documents:write
POST/api/v1/documents/{id}/sendSend a draft for signature (message, signing_order, auto_finalize).documents:write
POST/api/v1/documents/{id}/signers/{sid}/remindRemind a signer; new_link=true issues a fresh link.documents:write
GET/api/v1/documents/{id}/signers/{sid}/linkCurrent signing link and expires_at; ?regenerate=true revokes it and returns a new one without e-mailing.documents:read
POST/api/v1/documents/{id}/finalizeSeal (409 unless completed).documents:write
POST/api/v1/documents/{id}/cancelCancel; every link stops working.documents:write
GET/api/v1/documents/{id}/fileThe sealed PDF.documents:read
GET/api/v1/documents/{id}/certificateThe certificate of completion (PDF).documents:read
GET/api/v1/verify/{vid}Public verification record for a verification ID.none
POST/api/v1/verifyUpload any PDF: authentic, modified after sealing, tampered, flattened copy, unsigned…none
GET/api/v1/verify/{vid}/page/{n}/{digest}Does this page fingerprint belong to the sealed document?none
GET POST/api/v1/webhooksList or add endpoints (the secret is returned once).webhooks
GET DELETE/api/v1/webhooks/{id}Endpoint with its recent deliveries; remove it.webhooks
POST/api/v1/webhooks/{id}/testSend a ping event.webhooks
GET/api/v1/openapi.jsonOpenAPI 3.1 description.none

Follow a document

# status
curl https://sealique.com/api/v1/documents/$ID -H "Authorization: Bearer af_live_…"
# seal once everyone signed (not needed with auto_finalize)
curl -X POST https://sealique.com/api/v1/documents/$ID/finalize -H "Authorization: Bearer af_live_…"
# sealed PDF and certificate
curl -o signed.pdf https://sealique.com/api/v1/documents/$ID/file -H "Authorization: Bearer af_live_…"
curl -o certificate.pdf https://sealique.com/api/v1/documents/$ID/certificate -H "Authorization: Bearer af_live_…"

Errors

Every error has the same shape and an HTTP status that matches:

{"error": {"code": "invalid_state", "message": "The document can be finalized once every signer has signed.", "status": 409}, "mode": "live"}
CodeStatusMeaning
unauthorized401Missing, revoked or expired key.
insufficient_scope403The key lacks the scope the endpoint needs.
not_found404No such document, signer, endpoint or verification ID for this key.
invalid_request · invalid_json · invalid_file · invalid_cursor400A parameter, body, file or cursor is malformed.
invalid_state409The action does not fit the document's status.
payload_too_large413PDF over 30 MB or JSON body over 40 MB.
rate_limited429Retry after Retry-After seconds.
verification_unavailable503Verification by upload is not available on this server.

Webhooks

Register an https URL and we POST a JSON event whenever one of your documents changes. Live documents reach live endpoints, sandbox documents reach test endpoints. An endpoint takes the mode of the key that registers it, so a test key only manages test endpoints. The host must be public: URLs pointing at private, loopback or link-local addresses are rejected.

Events

  • document.sent
  • signer.opened
  • signer.signed
  • signer.declined
  • document.completed
  • document.finalized
  • document.canceled
  • ping — from the test button

Headers

  • X-Sealique-Signature: t=<unix>,v1=<hex> — HMAC-SHA256 of "{t}.{body}" with your endpoint secret
  • X-Sealique-Event — the event name
  • X-Sealique-Delivery — unique delivery ID (deduplicate on it)

Answer with any 2xx within a few seconds. Anything else is retried with back-off (1 min, 5 min, 30 min, 2 h, 12 h, 1 day) up to 6 attempts.

{
  "id": "delivery-uuid", "event": "signer.signed", "created_at": "2026-10-09T10:12:00+00:00", "mode": "live",
  "data": {
    "document": { "id": "…", "status": "sent", "signers": [ … ], "metadata": {"order": 1042}, … },
    "signer": { "id": "…", "name": "Aline Uwase", "email": "aline@example.com", "status": "signed", "signed_at": "…" }
  }
}

Verify the signature

import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# Flask / FastAPI / Django: pass the raw request body, not a re-serialised dict
ok = verify(WEBHOOK_SECRET, request.headers["X-Sealique-Signature"], request.get_data())

Embedding the signing experience

Hosted link

The simplest: send or show the signing_url. It works on any device, needs no account and expires with the document (30 days by default; regenerate it through the API).

return_url

Pass return_url when creating the document: after signing (or declining) the signer is sent there with ?event=signed|declined&document=<id>&signer=<id>. Confirm the outcome server-side through the status endpoint or a webhook, never from the query string alone.

iframe

Add your origins to the key's allowed_origins (https only). Signing pages of your documents then answer with Content-Security-Policy: frame-ancestors for those origins and may be framed. When the signer finishes, the page posts a message to the parent window:

window.addEventListener("message", (e) => {
  if (e.origin !== "https://sealique.com") return;
  if (e.data?.type === "sealique" && e.data.event === "signed") {
    // e.data.document, e.data.signer
  }
});

Identity confirmation: set require_otp and each signer must enter a one-time code e-mailed to them before the document opens. Every signer also accepts explicit consent statements; both are recorded in the audit trail and the certificate.

Verification

Three public, key-less ways to check a document. All are rate limited per IP (20 per minute).

# by file: authentic | modified_after_sealing | tampered | flattened_copy | unsigned_unknown | …
curl -F "file=@signed.pdf" https://sealique.com/api/v1/verify
# by verification ID (printed on every page)
curl https://sealique.com/api/v1/verify/VF-3F9A1C-7B2D0E
# one page's fingerprint (the QR in a page mark)
curl https://sealique.com/api/v1/verify/VF-3F9A1C-7B2D0E/page/3/1a2b3c4d

Outcome codes: authentic (byte-identical to the sealed file), modified_after_sealing (signatures intact but the file changed, e.g. annotations), tampered (a signature no longer validates), flattened_copy (a print or scan of a sealed document; text compared page by page), unsigned_unknown, unparseable, foreign_root, not_found, too_large.

Rate limits and idempotency

  • Per key: 120 requests per minute. Over it you get 429 rate_limited with a Retry-After header (seconds).
  • Verification endpoints: 20 per minute per IP.
  • Send an Idempotency-Key header (up to 128 characters, unique per key) with POST /api/v1/documents. A retry with the same key replays the first response (Idempotent-Replayed: true) instead of creating a second document.
  • Lists are cursor-paginated: limit (1–100, default 25) and next_cursor from the previous page.
  • PDFs up to 30 MB; metadata up to 8 KB; signing links valid 30 days.