DrakonSign API

Version 0.18.0 · OpenAPI document · interactive reference

REST API for DrakonSign — electronic signatures with a tamper-evident trust layer. When an envelope completes, DrakonSign produces a signed PDF, a separate completion-snapshot certificate and a hash-chained audit trail. When both the signing key and RFC 3161 timestamp authority are available, the PDF receives a self-issued PAdES B-T seal and the chain receives an RFC 3161 anchor only if the authority answers. A timestamp outage produces a recorded PAdES B-B degradation when the signature itself validates, and otherwise records a timestamp present but unverified, a signature that did not verify, or an unknown seal state; a sealing failure can leave recorded unsealed completion artifacts rather than an unqualified sealed outcome. The digital_seal value on the completion is measured from the produced file and is authoritative.

DrakonSign is a hosted service operated by Drakon Systems Ltd from London: the application and database run on Fly.io lhr, and live documents are held in a single-region London (lhr) Tigris bucket. Encrypted nightly backups go to a separate, globally distributed Tigris bucket with 30-day retention. Authenticate with an organisation API key in the X-API-Key header; the dashboard uses a session cookie, and billing is session-only so a leaked key cannot manage a subscription.

Authentication. Programmatic callers send an organisation API key in the X-API-Key header. Create one from Account → API keys in the dashboard; the key is shown once, at creation, and only its hash is stored. The dashboard itself uses a session cookie, and a few surfaces — billing, and anything that manages keys — are session-only by design, so a leaked API key cannot manage the subscription.

This page is rendered by the API itself and loads no scripts and no third-party assets.

Contents

Health

GET /api/health

Health Check

Responses

StatusMeaningBody
200Successful Response

GET /api/health/ready

Readiness

Responses

StatusMeaningBody
200Successful Response

GET /api/ping

Ping

Responses

StatusMeaningBody
200Successful Response

Auth

POST /api/auth/accept-invite

Accept Invite

Accept an org invitation and sign in. POST only, token in the body.

Never a GET: SameSite=Lax permits a top-level cross-site GET, so a GET accept would be CSRF-able from a link in any email — someone could be joined to a workspace by clicking a link that looked like something else. The token arrives in the URL fragment, which the static /invite page reads client-side and POSTs here, so it never reaches an access log.

On success this mints an ordinary session through the same _start_session the magic-link flow uses — there is no second session mechanism, and the new member's device gets the same new-sign-in alert every other sign-in produces. MFA cannot apply: the account is being created by this request, so it has no second factor to present.

Request body required

application/jsonAcceptInviteIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/auth/from-checkout

From Checkout

Instant entry after Stripe checkout: turn a paid session_id into a session.

401 if the checkout isn't a genuine paid session; 409 if it's paid but the provisioning webhook hasn't landed yet (the client should retry shortly).

Request body required

application/jsonFromCheckoutIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/auth/logout

Logout

Sign out THIS device, and only this device.

Before M11 this bumped a version number and signed every device out — a surprise for anyone who closed a session on a borrowed laptop and found their phone logged out too. "Sign out everywhere" is now a separate, deliberate action on the security page.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

GET /api/auth/me

Me

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/auth/mfa

Verify Mfa

Complete sign-in by presenting the second factor.

Failure is a single generic 401 for every cause — no challenge, expired challenge, attempts exhausted, wrong code, replayed timestep, spent recovery code, locked enrolment. The client cannot tell them apart, which is what keeps this from being an oracle about the account's state.

Parameters

NameInTypeNotes
ds_mfacookiestring or null

Request body required

application/jsonMfaVerifyIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/auth/request-link

Request Link

Email a sign-in link if the address maps to an account. Always 200.

Only the same indexed lookup runs before the response for either branch. For a candidate account, fenced minting, the atomic JTI/frozen-request commit and delivery all begin after the body is flushed. Within that background phase the provider still cannot run before the commit, and account closure is rechecked under the shared organization fence.

Request body required

application/jsonRequestLinkIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/auth/verify

Verify

Exchange a one-time login token for a session cookie.

When the account has a confirmed second factor this returns {"mfa_required": true} and an ds_mfa challenge cookie INSTEAD of a session: possession of the mailbox is one factor, and on its own it is exactly the thing MFA exists to stop being sufficient.

Request body required

application/jsonVerifyIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

Account security

GET /api/account/security

Get Security

The security page's whole state: MFA status, code count, live devices.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/security/mfa/begin

Begin Mfa

Stage a pending TOTP secret and return it with a locally rendered QR.

Returning the secret in plaintext here is unavoidable and correct — it is the one moment it has to reach the authenticator app — and it is why this route needs fresh reauthentication: the response IS the enrolment material.

Refuses outright when MFA is already on. Re-enrolling is a "replace my factor" operation and must go through disable (which requires the current factor) rather than quietly overwriting an active secret.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
403This session did not prove its credentials recently enough. The body is {"detail": {"code": "reauthentication_required", "message": ...}} — request a fresh sign-in link and retry.
409Two-step sign-in is already on; disable it first.
503Enrolment is closed while MFA_ENCRYPTION_KEY is rotated. Existing sign-ins, existing second factors and every other route are unaffected; retry shortly.
422Validation ErrorHTTPValidationError

POST /api/account/security/mfa/confirm

Confirm Mfa

Activate the pending secret and issue recovery codes, shown once.

Turning MFA on also signs every OTHER device out. If the reason someone is enrolling is "I think somebody else has access", leaving that somebody's session alive would defeat the entire exercise. This device stays signed in — it has just proved the new factor — but on a NEW cookie value, so a copy of the credential it was using dies with the change instead of surviving it.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonConfirmMfaIn

Responses

StatusMeaningBody
200Successful Response
403This session did not prove its credentials recently enough. The body is {"detail": {"code": "reauthentication_required", "message": ...}} — request a fresh sign-in link and retry.
400The code did not verify against the pending secret, or that secret has expired. Start enrolment again.
503Enrolment is closed while MFA_ENCRYPTION_KEY is rotated. Existing sign-ins, existing second factors and every other route are unaffected; retry shortly.
422Validation ErrorHTTPValidationError

POST /api/account/security/mfa/disable

Disable Mfa

Turn the second factor off. Requires fresh sign-in AND a current code.

Every other device is signed out and this one's cookie value is replaced, for the same reason as enabling: the account's authentication just got weaker, and a copy of the credential in use must not be one of the things that survives it.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonDisableMfaIn

Responses

StatusMeaningBody
200Successful Response
403This session did not prove its credentials recently enough. The body is {"detail": {"code": "reauthentication_required", "message": ...}} — request a fresh sign-in link and retry.
401The submitted second factor was not accepted. One generic message covers every cause, deliberately.
409Two-step sign-in is not on for this account.
422Validation ErrorHTTPValidationError

POST /api/account/security/mfa/recovery-codes

Regenerate Recovery Codes

Replace the whole recovery-code set. The old codes stop working at once.

Other sessions are deliberately NOT revoked here: this is a routine hygiene action ("I used one, give me a fresh sheet"), not a response to compromise, and signing someone's other devices out for it would train them to avoid doing it.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonRegenerateCodesIn

Responses

StatusMeaningBody
200Successful Response
403This session did not prove its credentials recently enough. The body is {"detail": {"code": "reauthentication_required", "message": ...}} — request a fresh sign-in link and retry.
401The submitted second factor was not accepted. One generic message covers every cause, deliberately.
409Two-step sign-in is not on for this account.
422Validation ErrorHTTPValidationError

GET /api/account/security/sessions

List Sessions

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/security/sessions/revoke-all

Revoke Other Sessions

Sign out every OTHER device, keeping this one.

Keeping the current session is the whole usability of the control: the person doing it is reacting to something and should not be thrown out of the page they are reacting on. "Sign out of this one too" is the ordinary Sign out button, one click away.

Deliberately NOT behind the reauthentication gate. Every gated action WEAKENS the account (bind a new factor, remove the existing one, reprint the codes); this one strengthens it. Someone who has just seen a sign-in they do not recognise must be able to act in one click, not go and fetch a fresh link from the mailbox that may itself be the problem.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

DELETE /api/account/security/sessions/{session_id}

Revoke Session

End one named session.

Ownership is checked against the CALLER'S user id, and a row belonging to anyone else is a flat 404 — not a 403. A distinguishable "exists but isn't yours" would turn session ids into a cross-tenant existence oracle, and the id itself tells an attacker nothing they can use.

Parameters

NameInTypeNotes
session_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
404No live session with that id belongs to the caller. A session owned by someone else is deliberately indistinguishable from one that never existed.
422Validation ErrorHTTPValidationError

Documents

GET /api/documents

List Documents

Org-scoped document list, newest first (the dashboard's home query).

Sync on purpose, like every handler here that touches the database: the Session is synchronous, so the connection-pool checkout and the query round-trip block the calling thread — that wait belongs in the threadpool, not on the event loop (see send_for_signing).

Parameters

NameInTypeNotes
limitqueryinteger
offsetqueryinteger
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Responsearray of DocumentListOut
422Validation ErrorHTTPValidationError

POST /api/documents

Upload Document

Sync on purpose: hashing, the capacity check and the blob write all block, so FastAPI must dispatch this to a threadpool (see send_for_signing). The body is read from UploadFile's spooled file with a hard cap so an oversized body can't exhaust the VM's memory.

Parameters

NameInTypeNotes
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

multipart/form-dataBody_upload_document_api_documents_post

Responses

StatusMeaningBody
201Successful ResponseDocumentOut
400The upload is not a valid supported PDF.
409The organization is closed.
413The upload exceeds the configured cap.
500Upload failed and storage artifact cleanup remains pending.
507The organization storage allowance or volume capacity is exhausted.
422Validation ErrorHTTPValidationError

GET /api/documents/cancellation-notices/manual-review

List Cancellation Manual Review

Return the authenticated org's sanitized intervention queue.

Recipient addresses, document titles, frozen provider HTML and raw provider errors are deliberately absent. The queue is owner-session-only; no API caller and no admin/member session may adjudicate an unknowable provider outcome.

Parameters

NameInTypeNotes
limitqueryinteger
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Responsearray of CancellationManualReviewOut
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}

Read Document

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseDocumentOut
422Validation ErrorHTTPValidationError

DELETE /api/documents/{document_id}

Delete Document

Attempt to erase an envelope in this request — irreversible on success.

Blobs and database rows are destroyed, not flagged. Two outcomes:

- {"status": "deleted"} — no signature was ever issued; the envelope and its entire record are gone. - {"status": "erased"} — a signature had been issued; the tamper-evident audit record is retained so the signature stays integrity-verifiable. The files and field values are destroyed; what remains is the hash-chained event log — event types, timestamps, network metadata, the envelope title and the SHA-256 fingerprints of the destroyed artifacts (DPA §9.2).

Session or API key: the DPA promises deletion "through the dashboard or API". API keys additionally need the can_delete scope, granted when the key is minted — a leaked everyday integration key must not be able to raze the org's envelopes. Idempotent on the residue — repeating the DELETE returns "erased" without touching the retained record.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Database erasure committed. If verified post-commit blob removal fails or raises, cleanup_pending=true and a recovery warning are returned.
403An API key without delete scope cannot erase documents.
404Tenant-scoped document not found.
409Erasure is blocked by provider-crossed lifecycle email or AI submission uncertainty that requires controlled support reconciliation.
503Lifecycle delivery coordination is busy; retry safely.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/ai/analysis

Analyse Document

Key clauses + risk flags for the *sender*, before they send.

Advisory only: never stored on the audit chain, the seal or the certificate. Returns available: false rather than an error whenever we cannot produce something useful — the org hasn't opted in, no key is configured, the PDF has no extractable text, or the model gave us nothing. AI must not be able to turn a working page into an error.

Sync on purpose (see send_for_signing): this blocks on Anthropic and must run in a threadpool, not on the event loop.

The result is cached against the original's SHA-256, so repeat views cost nothing and a client calling this in a loop cannot run up our Anthropic bill. Draft-only for the same reason — it is a pre-send aid, and a sent envelope's text does not change.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseClauseAnalysisOut
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/ai/suggest-fields

Suggest Document Fields

Auto-detect signing fields in the stored PDF and persist them.

Detection is deterministic text-anchor matching, so these land with placement_source="anchor". The "/ai/" in the path is a legacy name that integrations depend on — it does not mean a model ran.

Sync on purpose (see send_for_signing): the blocking storage read plus CPU-bound PDF text extraction must run in the threadpool, not on the event loop.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
201Successful Responsearray of FieldOut
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/anchor

Read Anchor

One RFC 3161 checkpoint token + metadata, for independent verification.

Without kind, returns the envelope's primary TERMINAL checkpoint — which for a completed envelope is the completion anchor this endpoint has always returned. Use GET /anchors for the whole set.

Sync on purpose — synchronous database work (see list_documents).

Parameters

NameInTypeNotes
document_idpathstringrequired
kindquerystring or null
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseChainAnchorOut
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/anchor

Create Anchor

Anchor a terminal prefix, or return an existing *validated* checkpoint.

Without kind this targets the envelope's primary TERMINAL checkpoint — for a completed envelope, the COMPLETED-event prefix this endpoint has always anchored. kind=signatures recovers the checkpoint the completion certificate reports; kind=declined|voided|expired recovers a terminal outcome whose checkpoint the TSA was not available for.

201 new checkpoint · 200 already checkpointed and valid · 409 no valid target prefix or stored token invalid · 502 TSA down/invalid new token · 503 trusted-root configuration missing. A corrupt/forged stored token is evidence and is never silently replaced. Retry applies only while no row exists for that kind.

Sync on purpose (see send_for_signing): anchoring blocks on the TSA round-trip (sealing._run joins its worker thread for up to the TSA timeout), so this must run in the threadpool, not on the event loop.

Parameters

NameInTypeNotes
document_idpathstringrequired
kindquerystring or null
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Existing checkpoint over the target prefix was cryptographically validated.ChainAnchorOut
201A new RFC 3161 checkpoint was created over the target event's prefix.
400An unknown checkpoint kind was requested.
409The retained history has no target event for the requested (or terminal) checkpoint kind, its exact prefix fails integrity, or the stored token is not exactly over that prefix and cannot be replaced in place.
502No anchor exists and the timestamp authority did not answer, or it returned a new token that failed immediate validation.
503Trusted TSA roots are absent or malformed, so neither an existing token nor a newly requested token can be independently validated.
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/anchors

Read Anchors

Every stored checkpoint for the envelope, tokens included.

This is the endpoint the completion certificate points a reader at: the certificate reproduces the signatures checkpoint it could describe when its bytes were frozen, and the completion checkpoint that covers the certificate's own SHA-256 is fetched from here.

Sync on purpose — synchronous database work (see list_documents).

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseChainAnchorListOut
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/audit

Read Audit Trail

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Responsearray of AuditEventOut
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/audit/verify

Verify Audit Trail

Recompute the full hash chain and validate every typed checkpoint.

An RFC 3161 checkpoint covers only the event prefix present at its target event. The response reports each checkpoint's covered prefix separately from the later internally chained events outside that timestamp, and the top-level anchor_* fields describe the envelope's primary TERMINAL checkpoint — completion for a completed envelope, the declined/voided/ expired checkpoint otherwise.

Sync on purpose (see send_for_signing): rehashing the full chain and the anchors' CMS signature validation are CPU-bound work that blocks the calling thread — it belongs in the threadpool, not on the event loop.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseAuditVerifyOut
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/certificate

Download Certificate

The certificate of completion (PDF).

The exact stored bytes must match the certificate's recorded SHA-256 before a DOWNLOADED event is appended or any content is served. A missing fingerprint, missing blob or mismatch returns 409. Successful responses are private/no-store and carry nosniff.

Sync + document-first lock, same as the signed download: this read also appends to the audit chain, so it must serialise against a concurrent erasure rather than race it.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200SHA-256-verified completion certificate; private/no-store and nosniff.string (binary)
409Document is not completed or certificate evidence is missing/corrupt.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/correct

Correct Document Route

Void this envelope and create a corrected DRAFT copy of it.

A sent envelope is evidence: its bytes, fields, recipients and signatures are never edited in place. This is the sanctioned way to fix a mistake — the original is cancelled and a new draft carries forward the exact original PDF, the recipient list and the field placements, with fresh ids, no signature values and no tokens. Both audit chains record the link and the reason, so the pair reads as one story from either end. The new draft is never auto-sent: review and send it like any other. Creating it reserves and consumes one new envelope immediately; that atomic reservation keeps the exact successor sendable if the allowance fills or subscription lapses later.

201 a corrected draft was created · 200 its previously created, still-editable draft is returned (idempotent — a double-click voids nothing twice, while undelivered durable notices may be retried) · 409 the envelope cannot be corrected (a draft is still editable, a completed envelope is never voided, an erased one has nothing to copy, the recorded successor is missing/erased/no longer a draft, or the stored original is missing or no longer matches its fingerprint) · 402 no new envelope is available (the source remains live and untouched) · 507 the copy would exceed the org's storage allowance.

Session or API key, and deliberately no extra key scope: this composes /void and a blob clone, both of which any org-scoped key may already do, so requiring the delete scope here would only push callers to the lower-level route. It destroys nothing — the source and every artifact and audit event it owns are preserved exactly as they were.

Sync on purpose (see send_for_signing): it blocks on a PDF read, a blob write, the document row lock and — after the commit — outbound email.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

application/jsonCorrectIn

Responses

StatusMeaningBody
201A new corrected draft was created.CorrectionOut
200Idempotent retry returning the existing corrected draft.CorrectionOut
400The correction reason or source PDF cannot be processed.
402The active/trial subscription has no envelope available. Nothing is voided or created. A structured response includes cleanup_pending=true and a recovery message if the temporary copy could not be removed.
404The organization/source is absent, or the correction committed and the replacement was concurrently erased; the latter returns structured correction_committed and consumed-allowance truth.
409The source is not correctable, or its one durable successor lineage has already been consumed and is unavailable. A structured response includes cleanup_pending=true and a recovery message if the temporary copy could not be removed.
503Lifecycle coordination is busy, or the correction committed and only durable cancellation-notification processing remains pending; retry safely to drain due work.
500Correction failed after publishing storage and immediate artifact cleanup could not be confirmed; cleanup_pending is returned.
507The corrected PDF copy cannot be stored within the organization's storage allowance; the source remains unchanged.
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/download

Download Signed Pdf

The signed artifact. Every successful download is recorded on the audit chain.

The exact stored bytes must match the recorded SHA-256 (signed_sha256) before a DOWNLOADED event is appended or any content is served. A missing fingerprint, missing blob or mismatch returns 409. Successful responses are private/no-store and carry nosniff.

Sync (threadpool) and holding the same document-first lock the writers take: without it a concurrent DELETE can commit its erasure between our status check and our audit append, and this route would then hand out bytes the API says are gone and append DOWNLOADED to a sealed chain (or 500 on a duplicated chain sequence). Locked, the two serialise: whichever wins, the other sees committed truth.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200SHA-256-verified completed signed PDF; its recorded seal state is the one measured on that file (see the completion payload's digital_seal). Private/no-store and nosniff.string (binary)
409Document is not completed or the signed artifact is missing/corrupt.
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/evidence

Download Evidence Bundle

The detached, write-once evidence bundle for a completed envelope.

This is the artifact that makes the evidence independently verifiable: the canonical audit records the hash chain covers, every RFC 3161 token in DER form, the authority's trust anchors as PEM, the artifact hashes and a deterministic verification procedure — none of which the human-readable certificate can carry, and none of which should require an authenticated call to Drakon to obtain.

Published once, after the completion checkpoint resolves; 409 until then, with the reason, because freezing a write-once artifact around "no completion checkpoint exists" while one is queued would be untrue.

Sync + document-first lock, same as the certificate download: it appends to the audit chain and must serialise against a concurrent erasure.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200SHA-256-verified evidence bundle: a PDF carrying the canonical evidence manifest as an embedded evidence.json attachment; private/no-store and nosniff.string (binary)
409The envelope is not completed, its completion checkpoint is still outstanding, or the stored bundle failed its integrity check.
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/evidence/editions

List Evidence Editions

Every published edition of this envelope's evidence bundle.

Publication is write-once per edition and additive across them, so a recipient can be holding edition 1 while edition 2 is current. Before this existed, editions() could enumerate the rows internally but nothing in the product could reach an earlier one — a superseded edition was, in practice, unobtainable the moment it was superseded. Read-only: it publishes nothing and appends nothing.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseEvidenceEditionsOut
404Unknown envelope for this tenant.
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/evidence/editions/{edition}

Download Evidence Edition

One historical edition, byte for byte, verified against its own SHA-256.

Deliberately NOT a publish path: asking for edition 1 must never mint edition 3. It serves what was published or 404s, which is what makes the word "immutable" mean something a caller can rely on.

Parameters

NameInTypeNotes
document_idpathstringrequired
editionpathintegerrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200SHA-256-verified bytes of ONE immutable edition, exactly as published.string (binary)
404Unknown envelope for this tenant, or no such edition.
409The stored bytes failed their integrity check.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/fields

Create Field

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

application/jsonFieldIn

Responses

StatusMeaningBody
201Successful ResponseFieldOut
422Validation ErrorHTTPValidationError

PATCH /api/documents/{document_id}/fields/{field_id}

Edit Field

Move/resize/retype/reassign a field on a draft (the visual composer's edit surface).

Sync on purpose — it waits on the document row lock (see create_signer).

Parameters

NameInTypeNotes
document_idpathstringrequired
field_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

application/jsonFieldPatch

Responses

StatusMeaningBody
200Successful ResponseFieldOut
422Validation ErrorHTTPValidationError

DELETE /api/documents/{document_id}/fields/{field_id}

Delete Field

Parameters

NameInTypeNotes
document_idpathstringrequired
field_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
204Successful Response
422Validation ErrorHTTPValidationError

GET /api/documents/{document_id}/original

Download Original Pdf

The original as uploaded — the composer renders placement on it.

No audit event: the sender previewing their own working copy is not part of the signing ceremony. DOWNLOADED is reserved for the signed evidence.

Sync on purpose (see send_for_signing): storage.get is a blocking read of a PDF up to the upload cap, and template-created drafts hit this route every time the composer opens — it must run in the threadpool, not stall the event loop.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Exact original bytes after SHA-256 verification under the organization-scoped document lock; private/no-store and nosniff.string (binary)
409Original storage or its recorded SHA-256 is missing, or the stored bytes fail integrity verification; private/no-store and nosniff.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/remind

Remind Signers

Queue and attempt a reminder for everyone whose turn it currently is.

Minting is how re-sending works (only the token's hash is stored), so each reminder supersedes the link already in the recipient's inbox. Link rotation, frozen provider requests and audit truth commit atomically; the response reports provider acceptance/pending/manual-review truth, never inbox delivery. Declared sync on purpose — the bounded outbox drain can call the provider.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseReminderOut
409The document has no currently eligible recipients to remind.
503Fresh links and durable reminder rows committed, but provider processing could not acquire its bounded lifecycle fence.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/save-as-template

Save As Template

Snapshot this DRAFT's PDF + recipients + fields into a reusable template.

The snapshot copies the blob — templates outlive their source envelope, so they must not share its storage (erasing the envelope destroys its files). Drafts only: once sent, an envelope is (becoming) evidence, and its content must not be copyable out from under the erasure rules. The service holds the org + document row locks, so a concurrent erasure serialises with the copy instead of racing it. Sync on purpose — file copy work belongs in the threadpool, not on the event loop (see send_for_signing).

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

application/jsonSaveAsTemplateIn

Responses

StatusMeaningBody
201Successful ResponseTemplateOut
404Tenant-scoped source document not found.
409The source is not a live draft or its original PDF is unavailable.
500Template copy failed and storage cleanup remains pending.
507The organization storage allowance or volume capacity is exhausted.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/send

Send For Signing

Finalize a draft and queue durable invitations for the first signer tier.

Sending is the metered action: it consumes one envelope from the org's plan allowance (402 when spent). Consumption, token hashes, frozen provider requests and SENT audit events commit together. Provider acceptance is a separate recoverable phase, so a pending or ambiguous email does not roll the envelope back or refund its allowance.

Declared sync on purpose: this handler blocks (email, and optionally an Anthropic call for the signer summary), so FastAPI must dispatch it to a threadpool. As an async def it ran on the event loop, where one slow AI call stalled every other request in the process — including a signer submitting a signature. Do not make this async without moving the blocking work off-loop.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful ResponseDocumentOut
400Draft composition is incomplete or invalid.
402The subscription is lapsed, its allowance is exhausted, or authoritative trial reconciliation is required.
404The organization or tenant-scoped document does not exist.
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/signers

Create Signer

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

application/jsonSignerIn

Responses

StatusMeaningBody
201Successful ResponseSignerOut
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/signers/me

Create Signer Me

Add the signed-in dashboard user as an ordinary signing recipient ("Add me").

Sync on purpose — it waits on the document row lock (see create_signer).

Session-only and body-less on purpose: the identity is derived server-side from the ACCEPTED session's user row, never from anything the client sends — a stale /me cache or a cross-tab sign-in can't put the wrong person on the envelope, and an API key (whose bearer is a program, not the person at the keyboard) is refused by the dependency with a 401. This creates a normal recipient and nothing more: they still get the signing email, magic link and one-time code like everyone else — never a shortcut past the OTP ceremony.

Routing lands in the NEXT signing group after the current maximum (an empty draft starts at 0): the sender reviewing their own pack signs after the recipients they are adding it for, and never silently jumps the queue.

Parameters

NameInTypeNotes
document_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
201Successful ResponseSignerOut
422Validation ErrorHTTPValidationError

DELETE /api/documents/{document_id}/signers/{signer_id}

Delete Signer

Remove a recipient from a draft, along with any fields assigned to them.

Sync on purpose — it waits on the document row lock (see create_signer).

Parameters

NameInTypeNotes
document_idpathstringrequired
signer_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
204Successful Response
422Validation ErrorHTTPValidationError

POST /api/documents/{document_id}/void

Void Document

Cancel an in-flight envelope. Terminal and irreversible, like decline.

Every outstanding signing link stops working the moment this commits; every recipient already contacted (including someone who signed earlier in a partially signed ceremony) gets a durable cancellation notice. Draft envelopes can't be voided (nothing was ever sent — delete them instead), and settled ones (completed, declined, expired) are history, not cancellable. Repeating a successful void drains any still-undelivered durable notices without adding another audit event. Sync on purpose — it may run a bounded post-commit outbox delivery phase.

Parameters

NameInTypeNotes
document_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body

application/jsonVoidIn or null

Responses

StatusMeaningBody
200The envelope is voided. Repeated calls are idempotent and only process due durable cancellation notices.VoidOut
409The envelope is not voidable in its current state.
404The tenant-scoped envelope is absent, or the void committed and a concurrent erasure removed it before response assembly; the latter returns structured void_committed truth.
503Lifecycle coordination is busy, or the void committed and only durable cancellation-notification processing remains pending; retry safely to drain due work.
422Validation ErrorHTTPValidationError

Templates

GET /api/templates

List Templates

Org-scoped template list, newest first (the 'New from template' picker).

Sync on purpose — the Session is synchronous, so the pool checkout and query block the calling thread; that wait belongs in the threadpool, not on the event loop (see documents.list_documents).

Parameters

NameInTypeNotes
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Responsearray of TemplateOut
422Validation ErrorHTTPValidationError

DELETE /api/templates/{template_id}

Delete Template

Delete a template — row and PDF blob.

Same credential rule as delete_document: sessions always may, API keys only with the can_delete scope — a leaked everyday integration key must not be able to strip the org's templates. (Templates are working material, not evidence, so nothing is retained.)

Parameters

NameInTypeNotes
template_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Template row deletion committed. If verified post-commit blob removal fails or raises, cleanup_pending=true and a recovery warning are returned.
403An API key without delete scope cannot delete templates.
404Tenant-scoped template not found.
422Validation ErrorHTTPValidationError

POST /api/templates/{template_id}/use

Use Template Route

Create a fresh DRAFT envelope from a template. Never auto-sends — the caller reviews (and can adjust) in the composer before sending. Sync on purpose: it blocks on file copy work and must run in FastAPI's threadpool, not on the event loop (see send_for_signing).

Parameters

NameInTypeNotes
template_idpathstringrequired
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Request body required

application/jsonUseTemplateIn

Responses

StatusMeaningBody
201Successful ResponseDocumentOut
400Recipient input is invalid.
404Tenant-scoped template not found.
409The template PDF/data is unavailable or the organization is closed.
500Draft copy failed and storage cleanup remains pending.
507The organization storage allowance or volume capacity is exhausted.
422Validation ErrorHTTPValidationError

Signing

GET /api/sign/receipt/{token}/certificate

Receipt Certificate

Parameters

NameInTypeNotes
tokenpathstringrequired
X-Receipt-Sessionheaderstring or null

Responses

StatusMeaningBody
200SHA-256-verified completion certificate for this signer.string (binary)
404Unknown, revoked or erased receipt credential.
401Explicit receipt-session confirmation is required.
409Completed evidence missing or integrity check failed.
422Validation ErrorHTTPValidationError

GET /api/sign/receipt/{token}/evidence

Receipt Evidence Bundle

Parameters

NameInTypeNotes
tokenpathstringrequired
editionqueryinteger or nullA specific immutable edition. Omit for the current one — which is what every existing caller does, and its behaviour is unchanged.
X-Receipt-Sessionheaderstring or null

Responses

StatusMeaningBody
200SHA-256-verified evidence bundle for this signer: a PDF carrying the canonical evidence manifest as an embedded evidence.json attachment.string (binary)
404Unknown, revoked or erased receipt credential.
401Explicit receipt-session confirmation is required.
409The completion checkpoint is still outstanding, or the stored bundle failed its integrity check.
422Validation ErrorHTTPValidationError

GET /api/sign/receipt/{token}/evidence/editions

Receipt Evidence Editions

Which editions this signer's own envelope has published.

Behind the same explicit artifact: "evidence" confirmation session as the download, and scoped to the one document the receipt resolves to. Read-only: it publishes nothing and appends no audit event, because listing what exists is not a download.

Parameters

NameInTypeNotes
tokenpathstringrequired
X-Receipt-Sessionheaderstring or null

Responses

StatusMeaningBody
200Successful ResponseEvidenceEditionsOut
404Unknown, revoked or erased receipt credential.
401Explicit receipt-session confirmation is required.
422Validation ErrorHTTPValidationError

POST /api/sign/receipt/{token}/session

Create Receipt Session

Exchange an explicit browser gesture for a short-lived header token.

Completion mail links only to the inert frontend receipt page. Automated GET/HEAD prefetch therefore cannot reach this POST, cannot obtain evidence, and cannot append a signer DOWNLOADED event.

Parameters

NameInTypeNotes
tokenpathstringrequired

Request body required

application/jsonReceiptSessionIn

Responses

StatusMeaningBody
200Successful ResponseReceiptSessionOut
404Unknown, revoked or erased receipt credential.
422Validation ErrorHTTPValidationError

GET /api/sign/receipt/{token}/signed

Receipt Signed Pdf

Parameters

NameInTypeNotes
tokenpathstringrequired
X-Receipt-Sessionheaderstring or null

Responses

StatusMeaningBody
200SHA-256-verified completed signed PDF for this completion signer; its recorded seal state is the one measured on that file (see the completion payload's digital_seal).string (binary)
404Unknown, revoked or erased receipt credential.
401Explicit receipt-session confirmation is required.
409Completed evidence missing or integrity check failed.
422Validation ErrorHTTPValidationError

POST /api/sign/session/decline

Session Decline

Parameters

NameInTypeNotes
X-Signing-Sessionheaderstring or null

Request body required

application/jsonDeclineIn

Responses

StatusMeaningBody
204Successful Response
401The signing session is missing, expired or revoked.
409The envelope reached a state that can no longer be declined.
503Lifecycle-email delivery coordination is busy; no decline transition was committed and the request can be retried.
422Validation ErrorHTTPValidationError

GET /api/sign/session/document

Session Document

Parameters

NameInTypeNotes
X-Signing-Sessionheaderstring or null

Responses

StatusMeaningBody
200Successful ResponseSignDocumentView
422Validation ErrorHTTPValidationError

GET /api/sign/session/pdf

Session Pdf

Parameters

NameInTypeNotes
X-Signing-Sessionheaderstring or null

Responses

StatusMeaningBody
200Exact original bytes after SHA-256 verification under the ceremony locks; private/no-store and nosniff.string (binary)
401The signing session is missing, expired or revoked.
409Original storage or its recorded SHA-256 is missing, or the stored bytes fail integrity verification; private/no-store and nosniff.
422Validation ErrorHTTPValidationError

POST /api/sign/session/sign

Session Sign

Parameters

NameInTypeNotes
X-Signing-Sessionheaderstring or null

Request body required

application/jsonSignIn

Responses

StatusMeaningBody
200Successful ResponseSignResultOut
400Required field values or signing consent are invalid.
401The signing session is missing, expired or revoked.
409The document or audit/original integrity state prevents signing.
500A completion-artifact write failed and storage cleanup remains pending; the signature transition was not committed.
503Lifecycle-email delivery coordination is busy; no signature transition was committed and the request can be retried.
507Completion artifacts could not be stored; the signature transition was rolled back and can be retried after capacity is restored.
422Validation ErrorHTTPValidationError

GET /api/sign/{token}

View Link

Parameters

NameInTypeNotes
tokenpathstringrequired

Responses

StatusMeaningBody
200Successful ResponseSignerViewOut
422Validation ErrorHTTPValidationError

POST /api/sign/{token}/decline

Decline

A magic link alone is not authority to void the legal ceremony.

Kept as an explicit compatibility refusal rather than silently removing the route: old clients receive actionable, non-terminal truth. The dependency still prevents token enumeration, but neither the reason nor the envelope is mutated until OTP verification created a signing session.

Parameters

NameInTypeNotes
tokenpathstringrequired

Request body required

application/jsonDeclineIn

Responses

StatusMeaningBody
403A magic link is not sufficient identity proof for a terminal decline; verify by OTP and use the signing-session route.
422Validation ErrorHTTPValidationError

POST /api/sign/{token}/otp

Send Otp

Parameters

NameInTypeNotes
tokenpathstringrequired

Responses

StatusMeaningBody
204Successful Response
202The OTP request and candidate hash are committed; provider acceptance remains pending or ambiguous.
404The signing ceremony or presented link is no longer live.
429OTP request budget exhausted or rate limited.
502The provider authoritatively rejected the staged OTP; any previous valid challenge remains usable.
422Validation ErrorHTTPValidationError

POST /api/sign/{token}/verify-otp

Verify Otp

Parameters

NameInTypeNotes
tokenpathstringrequired

Request body required

application/jsonOtpVerifyIn

Responses

StatusMeaningBody
200Successful ResponseSessionOut
422Validation ErrorHTTPValidationError

Billing

POST /api/billing/checkout

Start Checkout

Start a Stripe Checkout for the caller's own organisation.

This is the path a returning customer takes instead of a public payment link. The public links provision *new* workspaces and cannot safely attach to an existing one (the checkout email is unverified), so an existing account that goes through one ends up paid-but-unattached. Here the workspace is known before Stripe is ever called.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonStartCheckoutIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/billing/portal

Billing Portal

Open a Stripe Customer Portal session for the caller's org (upgrade/cancel).

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/billing/webhook

Stripe Webhook

Parameters

NameInTypeNotes
Stripe-Signatureheaderstring

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

Account

GET /api/account

Get Account

Parameters

NameInTypeNotes
X-API-Keyheaderstring or null
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

GET /api/account/activity

List Org Activity

Append-only member and API-key activity for the caller's org.

An activity log, NOT evidence: it is not hash-chained and makes no tamper-evidence claim. It is deliberately a separate table from the document audit trail — widening audit_events to host org events would have changed hash_payload() and invalidated every existing document chain, anchor and certificate.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

PATCH /api/account/ai

Set Ai Enabled

Turn the optional AI features on or off for the caller's organisation.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonAiSettingIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

GET /api/account/api-keys

List Api Keys

The org's API keys. Never the secrets — prefixes are display only.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/api-keys

Create Api Key

Mint an API key for the org. The plaintext is returned once and never again.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonCreateKeyIn

Responses

StatusMeaningBody
201Successful Response
422Validation ErrorHTTPValidationError

DELETE /api/account/api-keys/{key_id}

Revoke Api Key

Parameters

NameInTypeNotes
key_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/close

Close Account

Close the account and begin its destructive live-data purge in this request.

The DPA (§9.2) promises purge within 30 days of closure; doing it inside the closure itself satisfies that with nothing left to schedule or forget. Every envelope is erased under the standard criteria (signed ones leave their tamper-evident audit residue — see DELETE /api/documents/{id} for exactly what that retains), users and API keys are deleted, the organization row is redacted, and any Stripe subscription is cancelled best-effort. Filesystem deletion is verified; a storage or Stripe failure is returned explicitly and logged for recovery rather than silently described as complete. Irreversible.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonCloseAccountIn

Responses

StatusMeaningBody
200Successful Response
409Closure is blocked by provider-crossed lifecycle email or AI submission uncertainty that requires controlled support reconciliation.
503Organization lifecycle delivery coordination is busy; no closure is reported, so retry safely.
422Validation ErrorHTTPValidationError

GET /api/account/invites

List Invites

Pending invitations for the caller's org. Never the tokens.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/invites

Create Invite

Invite an address into the caller's workspace.

202, and the email is dispatched after the response, copying the shape of request-link so timing carries nothing. "Already a member here" and "already invited here" ARE distinguished — that is data the caller already owns. "This address belongs to another DrakonSign workspace" is NOT, and is never disclosed before the invitee accepts, or a cheap subscription becomes an enumeration oracle over the whole customer base.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonInviteIn

Responses

StatusMeaningBody
202Successful Response
422Validation ErrorHTTPValidationError

DELETE /api/account/invites/{invite_id}

Revoke Invite

Withdraw a pending invitation. The emailed link stops working at once.

Parameters

NameInTypeNotes
invite_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/invites/{invite_id}/resend

Resend Invite

Re-issue an invitation. This ROTATES the token — the old link dies.

Parameters

NameInTypeNotes
invite_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
202Successful Response
422Validation ErrorHTTPValidationError

GET /api/account/members

List Members

The workspace's members. Admin+, session-only.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

PATCH /api/account/members/{user_id}

Change Member Role

Change a member's role. Never your own; never above your own rank.

Parameters

NameInTypeNotes
user_idpathstringrequired
ds_sessioncookiestring or null

Request body required

application/jsonRoleChangeIn

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

DELETE /api/account/members/{user_id}

Remove Member

Remove a member. Their sessions, magic link and API keys all stop working.

Parameters

NameInTypeNotes
user_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

GET /api/account/members/{user_id}/api-keys

Member Api Keys

The live keys a member minted — what removing them would stop working.

Surfaced so the removal warning states facts. If the departing admin minted the key an integration runs on, that integration stops the instant they are removed, and the person clicking Remove should see which.

Parameters

NameInTypeNotes
user_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

Webhooks

GET /api/account/webhooks

List Webhooks

The workspace's destinations, plus what this deployment can do.

configured is what the dashboard branches on. A deployment with no WEBHOOK_ENCRYPTION_KEY reports false and the UI explains that instead of offering a Create button that would 503 — the same shape as ai_configured.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/webhooks

Create Webhook

Register a destination. The signing secret is returned ONCE, here.

A rejected destination (not HTTPS, an IP literal, a private DNS answer, a port that is not allowlisted) is a 400 carrying the specific reason, so the customer can fix it now rather than discovering it when their first envelope completes and nothing arrives.

Parameters

NameInTypeNotes
ds_sessioncookiestring or null

Request body required

application/jsonEndpointIn

Responses

StatusMeaningBody
201Successful Response
422Validation ErrorHTTPValidationError

GET /api/account/webhooks/deliveries

List Deliveries

Newest-first delivery log for the caller's own org.

endpoint_id filters; it is NOT a tenancy input. The org filter is applied first and always, from the session context, so a foreign endpoint id narrows the caller's own rows to none rather than widening them to somebody else's.

Parameters

NameInTypeNotes
endpoint_idquerystring or null
limitqueryinteger
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/webhooks/deliveries/{delivery_id}/replay

Replay Delivery

Queue the same event again, as a NEW delivery linked to its source.

202, not 200: the row is durable when this returns and the request goes out afterwards. The new delivery carries the same event_id (so the receiver's deduplication still recognises it) and frozen business-event data; only delivery_id changes, in both body and header, so the replay describes the envelope as it was without naming the wrong attempt.

The original row is never mutated. A failed delivery is a fact about what happened; rewriting it to pending would erase the record that we tried, exhausted the policy and gave up.

Parameters

NameInTypeNotes
delivery_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
202Successful Response
422Validation ErrorHTTPValidationError

DELETE /api/account/webhooks/{endpoint_id}

Delete Webhook

Remove a destination. The delivery history survives; the secret does not.

Soft delete: the endpoint row stays (deliveries hold a non-nullable FK onto it, and "we sent you the completion at 14:02 and you answered 200" is the record a support argument turns on) but it disappears from every customer-facing query and its sealed secret is destroyed in the same statement. What is retained is a record, not a usable credential to a destination the customer has told us to forget.

Parameters

NameInTypeNotes
endpoint_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/webhooks/{endpoint_id}/disable

Disable Webhook

Stop this destination: no new events, and the pending queue is cancelled.

Cancelling the backlog rather than holding it is deliberate — a customer who switches a misbehaving destination off does not want a stampede of stale events the moment they switch it back on. Anything they still want is an explicit replay from the delivery log.

Parameters

NameInTypeNotes
endpoint_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/webhooks/{endpoint_id}/enable

Enable Webhook

Re-enable a destination. FUTURE events only.

The destination is re-validated here, not trusted from the row: it may have been re-pointed at an internal address while it sat disabled, and enabling is exactly the moment to find that out.

Parameters

NameInTypeNotes
endpoint_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

POST /api/account/webhooks/{endpoint_id}/rotate-secret

Rotate Webhook Secret

Mint a new signing secret. **The old one stops verifying immediately.**

There is no overlap window, deliberately — see webhooks.rotate_secret for why a grace period is the wrong answer to "I think my secret leaked". The safe procedure is the customer's to run: accept both values in your receiver, rotate here, then drop the old one.

Parameters

NameInTypeNotes
endpoint_idpathstringrequired
ds_sessioncookiestring or null

Responses

StatusMeaningBody
200Successful Response
422Validation ErrorHTTPValidationError

Schemas

AcceptInviteIn

FieldTypeNotes
tokenstringrequired
namestring or null

AiSettingIn

FieldTypeNotes
ai_enabledbooleanrequired

AuditEventOut

FieldTypeNotes
sequenceintegerrequired
event_typestringrequired
actorstringrequired
signer_idstring or nullrequired
ipstring or nullrequired
user_agentstring or nullrequired
timestamp_utcstringrequired
payload_jsonstring or nullrequired
prev_hashstringrequired
hashstringrequired

AuditVerifyOut

FieldTypeNotes
intactbooleanrequired
first_broken_sequenceinteger or nullrequired
event_countintegerrequired
anchoredboolean default False
anchor_expectedbooleanTrue when the retained audit history contains a terminal event (completed, declined, voided or expired), including erased residues. default False
anchor_missing_recordedbooleanTrue when the chain immutably records a fail-open checkpoint failure FOR THE PRIMARY TERMINAL KIND. A degradation recorded for another kind (for example `signatures`) sets `any_checkpoint_missing_recorded` instead — presenting it here would describe one checkpoint's failure as the terminal one's. default False
any_checkpoint_missing_recordedbooleanTrue when the chain records a fail-open checkpoint failure for ANY kind. Read `checkpoints[]` to see which. default False
state_consistentbooleanFalse when the retained evidence contradicts itself — more than one terminal transition, a document status that disagrees with the audit history, or a stored checkpoint of a kind this release cannot explain. default True
anomaliesarray of stringPlain statements of every inconsistency found. Empty when consistent.
anchor_event_countinteger or nullNumber of audit events in the RFC 3161-timestamped terminal prefix.
unanchored_suffix_countintegerLater events that recompute internally but are outside the largest VALIDATED timestamped prefix across all checkpoints. When no checkpoint validates — none stored, or every stored token refused — that prefix is empty and this equals event_count: nothing is covered. default 0
anchored_atstring or null
anchor_tsastring or null
anchor_validboolean or null
anchor_validation_statusnot_present | valid | invalid | unverifiable_configuration default not_present
terminal_checkpoint_kindcompletion | declined | voided | expired or nullWhich terminal checkpoint this envelope's outcome calls for, if any.
checkpointsarray of CheckpointOutEvery checkpoint kind relevant to this envelope, present or not.

Body_upload_document_api_documents_post

FieldTypeNotes
titlestringrequired
filestringrequired

CancellationManualReviewOut

Sanitized org-scoped delivery view; never includes frozen email HTML.

FieldTypeNotes
idstringrequired
document_idstringrequired
notice_kindstringrequired
statusstringrequired
created_atstring (date-time)required
manual_review_atstring (date-time) or null
attempt_countintegerrequired
provider_boundary_crossedbooleanrequired
last_attempt_atstring (date-time) or null
idempotency_expires_atstring (date-time) or null
payload_sha256stringrequired
provider_message_idstring or null
reason_codestringrequired
pii_redactedbooleanrequired
support_requiredboolean default True

ChainAnchorListOut

FieldTypeNotes
document_idstringrequired
anchorsarray of ChainAnchorOut default []

ChainAnchorOut

FieldTypeNotes
document_idstringrequired
head_hashstringrequired
event_countintegerrequired
tsa_urlstringrequired
created_atstring (date-time)required
token_b64stringrequired
anchor_kindstring default completion
target_event_typestring default completed
target_event_sequenceinteger default 0

CheckpointOut

One typed RFC 3161 checkpoint over the audit trail, described precisely.

FieldTypeNotes
kindstringrequired
kind_knownbooleanFalse when the stored checkpoint kind is not one this release defines. Such a row is reported as an anomaly and never as a valid checkpoint. default True
target_event_typestringrequiredThe audit event type this checkpoint's token was taken over.
presentbooleanrequiredTrue when a stored token exists for this kind.
expectedbooleanrequiredTrue when the retained audit history contains the target event, so a checkpoint of this kind should exist.
missing_recordedbooleanTrue when the chain immutably records that this checkpoint's fail-open attempt failed. default False
obligation_pendingbooleanTrue when a durable obligation for this checkpoint is still outstanding, so it is expected to arrive. The obligation is written in the same transaction as the outcome that owes it, so it survives any later failure. default False
validation_statusnot_present | valid | invalid | unverifiable_configuration default not_present
validboolean or null
reasonstring or null
target_event_sequenceinteger or null
covered_event_countinteger or nullNumber of audit events inside this checkpoint's timestamped prefix.
suffix_event_countinteger or nullEvents after this checkpoint's prefix — internally chained, outside its timestamp.
head_hashstring or null
tsa_urlstring or null
attested_atstring or nullTSA-attested generation time. Present only for a token that validated.

ClauseAnalysisOut

Pre-send reading aid for the sender (M10), never stored on the envelope.

available is False whenever the org has not opted into AI, no key is configured, the PDF has no extractable text, or the model returned nothing usable. Callers render the panel only when it is True — AI is never allowed to turn a working page into an error.

FieldTypeNotes
availablebooleanrequired
clausesarray of ClauseOut default []
risksarray of RiskOut default []

ClauseOut

FieldTypeNotes
titlestringrequired
detailstringrequired

CloseAccountIn

FieldTypeNotes
confirmboolean default False

ConfirmMfaIn

FieldTypeNotes
codestringrequired

CorrectIn

FieldTypeNotes
reasonstringrequired

CorrectionOut

The result of a void-and-copy correction.

Reports what actually happened rather than assuming the happy path: created is False when an earlier request already produced this draft (the idempotent replay), voided_now is False when the source was already settled and had nothing left to cancel. Notification counters report only provider acceptance, pending work and terminal/manual-review states; they never imply that a recipient received or opened an email.

FieldTypeNotes
createdbooleanrequired
source_idstringrequired
source_statusstringrequired
voided_nowbooleanrequired
reasonstring or null
provider_acceptedinteger default 0
notify_permanent_failedinteger default 0
notify_pendinginteger default 0
notify_manual_reviewinteger default 0
notify_outcome_unknowninteger default 0
cleanup_pendingboolean default False
warningstring or null
draftDocumentOutrequired

CreateKeyIn

FieldTypeNotes
namestringrequired
can_deleteboolean default False

DeclineIn

FieldTypeNotes
reasonstring or null

DisableMfaIn

FieldTypeNotes
codestringrequired

DocumentListOut

Summary row for the dashboard list — field rectangles stay on the detail endpoint.

FieldTypeNotes
idstringrequired
titlestringrequired
statusstringrequired
page_countinteger or nullrequired
created_atstring (date-time)required
completed_atstring (date-time) or null
signersarray of SignerOut default []

DocumentOut

FieldTypeNotes
idstringrequired
titlestringrequired
statusstringrequired
original_sha256string or nullrequired
signed_sha256string or null
page_countinteger or nullrequired
created_atstring (date-time)required
completed_atstring (date-time) or null
original_storage_recordedboolean default False
corrected_from_document_idstring or null
corrected_by_document_idstring or null
has_correction_lineageboolean default False
correction_consumedboolean default False
signersarray of SignerOut default []
fieldsarray of FieldOut default []

DocumentStatus

No declared fields.

EndpointIn

A destination request.

There is deliberately no org_id and no secret: the tenant comes from the session context and the secret is minted server-side. A caller supplying its own secret would be choosing the strength of the credential that authenticates us to them, which is not theirs to weaken.

FieldTypeNotes
urlstringrequired
descriptionstring default
event_typesarray of string

EvidenceEditionOut

One immutable, published edition of the detached evidence bundle.

Editions are additive: a bundle published while a checkpoint was still owed is never rewritten when that checkpoint lands, because a recipient may already hold its bytes. A recipient must therefore be able to obtain the edition they were given, not merely the newest one, which is why this list and the per-edition download exist at all.

FieldTypeNotes
editionintegerrequired1-based; the highest edition is the current one.
sha256stringrequiredSHA-256 of the exact published bytes.
manifest_schemastringrequired
supersedes_sha256string or nullSHA-256 of the edition this one replaces. ISSUER-AUTHENTICATED: the bundle's publication signature covers it, and no timestamp authority attests it.
published_atstring (date-time) or null
currentbooleanrequiredTrue for the edition GET /{id}/evidence serves right now.

EvidenceEditionsOut

FieldTypeNotes
editionsarray of EvidenceEditionOut default []
lineage_assurancestringWhat binds one edition to its predecessor. `issuer-authenticated` — the publication signature over the manifest, which detects alteration by anyone without Drakon's sealing key and is not evidence against Drakon. The succession is NOT independently timestamped: an edition cannot contain the record of its own publication. default issuer-authenticated

FieldIn

FieldTypeNotes
pageintegerrequired
xnumberrequired
ynumberrequired
widthnumberrequired
heightnumberrequired
field_typeFieldType default signature
requiredboolean default True
signer_idstring or null

FieldOut

FieldTypeNotes
idstringrequired
pageintegerrequired
xnumberrequired
ynumberrequired
widthnumberrequired
heightnumberrequired
field_typestringrequired
requiredbooleanrequired
signer_idstring or nullrequired
placement_sourcestringrequired

FieldPatch

Partial edit of a draft field. Sending signer_id: null unassigns it; combined-rect and page-range validation happens in the service, where the merged result is known.

FieldTypeNotes
pageinteger or null
xnumber or null
ynumber or null
widthnumber or null
heightnumber or null
field_typeFieldType or null
requiredboolean or null
signer_idstring or null

FieldType

No declared fields.

FromCheckoutIn

FieldTypeNotes
session_idstringrequired

HTTPValidationError

FieldTypeNotes
detailarray of ValidationError

InviteIn

FieldTypeNotes
emailstring (email)required
rolestring default member

MfaVerifyIn

FieldTypeNotes
codestringrequired

OtpVerifyIn

FieldTypeNotes
codestringrequired

ReceiptSessionIn

Explicit user gesture required before completed evidence can be fetched.

FieldTypeNotes
confirm_downloadbooleanrequired
artifactsigned | certificate | evidencerequired

ReceiptSessionOut

FieldTypeNotes
session_tokenstringrequired
expires_in_secondsintegerrequired

RegenerateCodesIn

FieldTypeNotes
codestringrequired

ReminderOut

Durable reminder-queue and email-provider truth for one request.

FieldTypeNotes
remindedintegerrequiredCompatibility alias for provider_accepted; never a delivery or read receipt.
failedintegerrequiredAuthoritative permanent payload/content rejections.
queuedintegerrequiredDurable reminder rows committed with freshly rotated links.
provider_acceptedintegerrequiredRequests accepted by the email provider, not proven delivered.
pendingintegerrequiredQueued, leased, submitting or ambiguous provider outcomes.
manual_reviewintegerrequiredRequests requiring controlled support reconciliation.
skippedintegerrequiredRows cancelled before provider submission because the link became stale.
request_idstring or null

RequestLinkIn

FieldTypeNotes
emailstring (email)required

RiskOut

FieldTypeNotes
severitystringrequired
titlestringrequired
detailstringrequired

RoleChangeIn

FieldTypeNotes
rolestringrequired

SaveAsTemplateIn

FieldTypeNotes
namestringrequired

SessionOut

FieldTypeNotes
session_tokenstringrequired

SignDocumentView

Post-OTP view: the document metadata + this signer's own fields.

FieldTypeNotes
document_titlestringrequired
page_countinteger or nullrequired
fieldsarray of FieldOutrequired
ai_summarystring or null
consent_text_versionstringrequired
consent_textstringrequired

SignIn

FieldTypeNotes
field_valuesobject of string default {}
consentboolean default False
consent_text_versionstringrequired

SignResultOut

Committed ceremony state after a successful signing submission.

FieldTypeNotes
statusDocumentStatusrequired

SignerIn

FieldTypeNotes
namestringrequired
emailstring (email)required
roleSignerRole default signer
routing_indexinteger default 0

SignerOut

FieldTypeNotes
idstringrequired
namestringrequired
emailstringrequired
rolestringrequired
routing_indexintegerrequired
statusstringrequired

SignerRole

No declared fields.

SignerViewOut

Pre-OTP metadata shown when a signer opens their link (no PDF, no fields yet).

FieldTypeNotes
document_titlestringrequired
signer_namestringrequired
organizationstringrequired
page_countinteger or nullrequired
statusstringrequired

StartCheckoutIn

What the browser is allowed to choose: a tier and a billing interval.

Deliberately nothing else. The organisation the checkout is for comes from the session cookie and is stamped on the Stripe session server-side, so no request body can point a purchase at a workspace the caller is not in.

FieldTypeNotes
tierstringrequired
intervalstring default monthly

TemplateOut

FieldTypeNotes
idstringrequired
namestringrequired
page_countinteger or nullrequired
created_atstring (date-time)required
signersarray of TemplatePlaceholderOut default []
field_countinteger default 0
damagedboolean default False

TemplatePlaceholderOut

FieldTypeNotes
namestringrequired
emailstringrequired
rolestringrequired
routing_indexintegerrequired

UseTemplateIn

FieldTypeNotes
titlestringrequired
signersarray of UseTemplateSignerIn default []

UseTemplateSignerIn

FieldTypeNotes
namestringrequired
emailstring (email)required

ValidationError

FieldTypeNotes
locarray of string | integerrequired
msgstringrequired
typestringrequired
inputany
ctxobject

VerifyIn

FieldTypeNotes
tokenstringrequired

VoidIn

FieldTypeNotes
reasonstring or null

VoidOut

A void plus durable cancellation-notice delivery truth.

FieldTypeNotes
idstringrequired
titlestringrequired
statusstringrequired
original_sha256string or nullrequired
signed_sha256string or null
page_countinteger or nullrequired
created_atstring (date-time)required
completed_atstring (date-time) or null
original_storage_recordedboolean default False
corrected_from_document_idstring or null
corrected_by_document_idstring or null
has_correction_lineageboolean default False
correction_consumedboolean default False
signersarray of SignerOut default []
fieldsarray of FieldOut default []
voided_nowboolean default True
provider_acceptedinteger default 0
notify_permanent_failedinteger default 0
notify_pendinginteger default 0
notify_manual_reviewinteger default 0
notify_outcome_unknowninteger default 0
warningstring or null