API reference
Generated from the router. If an endpoint takes a field, it is in this table, because the table and the validator read the same declaration.
Base URL
https://api.signsealer.com. Everything is JSON, everything takes
Authorization: Bearer mk_live_…, and every response carries
x-request-id.
Account
GET /v1/account
Show the account this key belongs to
The first call any integration makes: prove the credential works and show the operator the name of the account they just connected.
{
"tenant_id": "3a1f…",
"name": "Fern Hollow Stables",
"slug": "fern-hollow",
"key_label": "Booking system",
"scopes": [
"signing:read",
"signing:write"
]
}
Templates
GET /v1/templates
List templates
Every version of every template, newest first, with whether each is published and how many documents were prepared from it.
{
"templates": [
{
"template_id": "8c2e…",
"code": "liability-waiver",
"name": "Liability waiver",
"kind": "waiver",
"version": 2,
"is_published": true,
"published_at": "2026-10-03T14:22:09Z",
"variables": [
"participant_name",
"date",
"emergency_contact"
],
"grants": {},
"documents": 412,
"is_latest": true
}
]
}
POST /v1/templates
Create a template
Creates version 1, as a draft. A template is not usable until it is published, and publishing is what freezes the text people will have signed.
| Field | Type | Constraints | |
|---|---|---|---|
| code | string | required | matches ^[a-z][a-z0-9_-]*$ |
| name | string | required | 1–200 chars |
| kind | "nda" | "waiver" | "contract" | "consent" | "policy" | "other" | required | |
| body | string | required | 1–∞ chars |
| variables | string[] | optional | |
| grants | object | optional | |
| consent_text | string | null | optional |
{
"template_id": "8c2e…"
}
Documents
GET /v1/documents
List documents
Filter with status (a state, or open for draft and sent) and q (title, subject or signer).
{
"documents": [
{
"document_id": "d41c…",
"title": "Liability waiver — Dana Reyes",
"status": "sent",
"template_code": "liability-waiver",
"template_version": 2,
"subject_name": null,
"signers": 2,
"signed": 1,
"waiting_on": "Sam Manager",
"sequential": true,
"expires_at": "2026-10-17T00:00:00Z",
"sent_at": "2026-10-03T14:22:09Z",
"completed_at": null,
"void_reason": null
}
]
}
GET /v1/documents/{id}
Show one document
The document, its signers, and the whole event trail with the text hash each event was made against.
{
"document_id": "d41c…",
"title": "Liability waiver — Dana Reyes",
"status": "completed",
"body": "I, Dana Reyes, …",
"body_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"template_code": "liability-waiver",
"template_version": 2,
"grants": {},
"sequential": false,
"sent_at": "2026-10-03T14:22:09Z",
"completed_at": "2026-10-03T14:22:09Z",
"voided_at": null,
"void_reason": null,
"integrity_ok": true,
"signers": [
{
"signer_id": "5f0a…",
"email": "dana@example.com",
"full_name": "Dana Reyes",
"role": "signer",
"sign_order": 1,
"status": "signed",
"party_id": null,
"consented_at": "2026-10-03T14:22:09Z",
"signed_at": "2026-10-03T14:22:09Z",
"declined_at": null,
"decline_reason": null,
"token_expires_at": "2026-10-17T00:00:00Z"
}
],
"events": [
{
"event_id": "e1…",
"kind": "sent",
"occurred_at": "2026-10-03T14:22:09Z",
"signer_email": null,
"ip": null,
"user_agent": null,
"against_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"detail": null
},
{
"event_id": "e2…",
"kind": "consented",
"occurred_at": "2026-10-03T14:22:09Z",
"signer_email": "dana@example.com",
"ip": "203.0.113.9",
"user_agent": "Mozilla/5.0 …",
"against_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"detail": null
},
{
"event_id": "e3…",
"kind": "signed",
"occurred_at": "2026-10-03T14:22:09Z",
"signer_email": "dana@example.com",
"ip": "203.0.113.9",
"user_agent": "Mozilla/5.0 …",
"signature_method": "typed",
"against_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"detail": null
}
]
}
POST /v1/documents
Prepare a document from a template
Renders the published template with the values given and stores the result on the document. Every declared variable needs a value: a contract that ships with a placeholder still in it is worse than one that failed to send.
| Field | Type | Constraints | |
|---|---|---|---|
| template_code | string | required | |
| title | string | optional | 0–200 chars |
| values | object | optional | |
| expires_at | string (date-time) | null | optional | |
| sequential | boolean | optional |
{
"document_id": "d41c…"
}
POST /v1/documents/{id}/send
Send a document for signature
Returns one signing link per signer, once. The tokens are not stored, only their hashes, so a lost link is replaced by a reminder rather than looked up.
| Field | Type | Constraints | |
|---|---|---|---|
| signers | object[] | required | 1–50 items |
{
"links": [
{
"signer_id": "5f0a…",
"email": "dana@example.com",
"token": "3f9c…64 hex characters"
}
]
}
POST /v1/documents/{id}/void
Void a document
Needs a reason, which goes on the audit trail. An executed agreement cannot be voided: it is terminated by agreement, not by deletion.
| Field | Type | Constraints | |
|---|---|---|---|
| reason | string | required | 1–500 chars |
{
"voided": true
}
GET /v1/documents/{id}/certificate
Fetch the certificate of completion
Issued when the document completes, frozen at issue and hashed. still_matches_record re-derives it from the live record and reports any divergence rather than correcting it.
{
"certificate_id": "c7d0…",
"verification_code": "K7QP3MTV9XBD2FHR",
"issued_at": "2026-10-03T14:22:09Z",
"document_sha256_hex": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"certificate_sha256_hex": "592f…",
"still_matches_record": true,
"payload": {
"issuer": {
"tenant": "Fern Hollow Stables",
"tenant_id": "3a1f…"
},
"document": {
"id": "d41c…",
"title": "Liability waiver — Dana Reyes",
"sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"completed_at": "2026-10-03T14:22:09Z",
"template": {
"code": "liability-waiver",
"version": 2
}
},
"signers": [
{
"full_name": "Dana Reyes",
"email": "dana@example.com",
"role": "signer",
"sign_order": 1,
"status": "signed",
"consented_at": "2026-10-03T14:22:09Z",
"signed_at": "2026-10-03T14:22:09Z",
"signature": {
"method": "typed",
"value_sha256": "017a…",
"signed_against_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"ip": "203.0.113.9"
}
}
],
"events": [
{
"kind": "signed",
"occurred_at": "2026-10-03T14:22:09Z",
"signer": "dana@example.com",
"ip": "203.0.113.9",
"against_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324"
}
]
}
}
GET /v1/documents/{id}/pdf
Download the sealed PDF
The certificate as a PAdES-sealed PDF, with an RFC 3161 timestamp over the signature. Returns base64 bytes and the filename to save them under.
{
"filename": "certificate-K7QP3MTV9XBD2FHR.pdf",
"content_type": "application/pdf",
"seal": {
"commonName": "SignSealer",
"publiclyTrusted": false,
"timestamped": true
},
"bytes_base64": "JVBERi0xLjcK…"
}
GET /v1/documents/{id}/bundle
Download the evidence bundle
The sealed PDF with the evidence package printed after the audit trail: every message sent about the document, every webhook delivered, and the record's state when the file was made, with the package's SHA-256 on the last page. One PAdES-sealed file. Returns base64 bytes and the filename to save them under.
{
"filename": "evidence-bundle-K7QP3MTV9XBD2FHR.pdf",
"content_type": "application/pdf",
"seal": {
"commonName": "SignSealer",
"publiclyTrusted": false,
"timestamped": true
},
"bytes_base64": "JVBERi0xLjcK…"
}
Workflows
GET /v1/workflows
List workflows
A workflow is a published template plus the rules for running it: who signs in what order, how long a link lives, and whether it has a public address.
{
"workflows": [
{
"id": "w9b1…",
"code": "waiver",
"name": "Kayak rental waiver",
"kind": "waiver",
"template_code": "liability-waiver",
"template_published": true,
"signer_roles": [
{
"label": "Participant",
"role": "signer",
"order": 1,
"participant": true
}
],
"sequential": false,
"countersign": false,
"expires_days": 7,
"remind_after_days": 3,
"public_slug": "waiver",
"intro": "Read it, answer two questions, sign.",
"form_fields": [
{
"key": "emergency_contact",
"label": "Emergency contact",
"required": true
}
],
"active": true,
"drafted_by_run": null,
"created_at": "2026-10-03T14:22:09Z",
"updated_at": "2026-10-03T14:22:09Z",
"started": 412,
"completed": 398
}
]
}
POST /v1/workflows/{id}/start
Start a workflow for one participant
Prepares the document, sends it to the participant and to every fixed role, and returns the tokens once.
| Field | Type | Constraints | |
|---|---|---|---|
| string (email) | required | ||
| full_name | string | required | 1–200 chars |
| phone | string | optional | 0–40 chars |
| fields | object | optional | |
| values | object | optional | |
| external_ref | string | optional | 0–200 chars |
{
"document_id": "d41c…",
"title": "Kayak rental waiver — Dana Reyes",
"expires_at": "2026-10-10T14:22:09Z",
"signers": [
{
"signer_id": "5f0a…",
"email": "dana@example.com",
"full_name": "Dana Reyes",
"role": "signer",
"participant": true,
"token": "3f9c…"
},
{
"signer_id": "5f0b…",
"email": "manager@fernhollow.example",
"full_name": "Sam Manager",
"role": "signer",
"participant": false,
"token": "7a21…"
}
]
}
Subjects
PUT /v1/subjects
Import a reservation or activity, and build its packet
Keyed on (provider, external_id), so importing the same reservation twice is one reservation, which is why this needs no idempotency key. Participants and the packet are built in the same call, because a caller who imported a reservation always wants to know what is now required.
| Field | Type | Constraints | |
|---|---|---|---|
| kind | "reservation" | "activity" | optional | |
| provider | string | optional | matches ^[a-z][a-z0-9_-]*$ |
| external_id | string | required | 1–200 chars |
| label | string | optional | 0–200 chars |
| starts_on | string | required | matches ^\d{4}-\d{2}-\d{2}$ |
| ends_on | string | null | optional | matches ^\d{4}-\d{2}-\d{2}$ |
| facts | object | optional | |
| participants | object[] | optional | 0–60 items |
{
"subject_id": "s2e4…",
"packet": {
"packet_id": "p6c9…",
"code": "RES-88213",
"added": 2,
"dropped": 0,
"state": "open",
"required": 2,
"completed": 0,
"outstanding": 2,
"not_started": 2
}
}
Packets
GET /v1/packets/{id}
Show a packet
Who owes what, where each one has got to, and a readiness state a door lock can wait on.
{
"packet_id": "p6c9…",
"code": "RES-88213",
"status": "open",
"ready_at": null,
"subject": {
"id": "s2e4…",
"kind": "reservation",
"label": "Reyes party, 4 Oct",
"provider": "deckpos",
"external_id": "RES-88213",
"starts_on": "2026-10-04",
"ends_on": "2026-10-04",
"facts": {}
},
"readiness": {
"required": 2,
"completed": 1,
"outstanding": 1,
"blocked": 0
},
"items": [
{
"item_id": "i1…",
"requirement": "waiver",
"workflow": "waiver",
"required": true,
"participant": {
"id": "pa1…",
"full_name": "Dana Reyes",
"email": "dana@example.com",
"is_minor": false,
"signs_for_them": null
},
"document_id": "d41c…",
"status": "completed"
}
]
}
POST /v1/packets/items/{id}/start
Start one packet item
Renders the item's workflow for the person who owes it. A minor's document is started for their guardian, because the guardian is who signs.
{
"document_id": "d41d…",
"title": "Kayak rental waiver — Dana Reyes",
"expires_at": "2026-10-10T14:22:09Z",
"signers": [
{
"signer_id": "5f0c…",
"email": "dana@example.com",
"full_name": "Dana Reyes",
"role": "signer",
"participant": true,
"token": "3f9c…"
}
]
}
Webhooks
GET /v1/webhooks
List webhook endpoints
{
"endpoints": [
{
"id": "h4a7…",
"url": "https://pms.example.com/hooks/signsealer",
"label": "Booking system",
"events": [
"signing.completed",
"signing.declined"
],
"active": true,
"failures": 0,
"paused_at": null,
"last_delivered_at": "2026-10-03T14:22:09Z",
"created_at": "2026-10-03T14:22:09Z",
"queued": 0,
"dead": 0
}
]
}
POST /v1/webhooks
Add a webhook endpoint
Returns the signing secret once and never again. It is sealed at rest; a caller who loses it replaces the endpoint.
| Field | Type | Constraints | |
|---|---|---|---|
| url | string (uri) | required | matches ^https:// |
| events | string[] | optional | 1–30 items |
| label | string | optional | 0–80 chars |
{
"id": "h4a7…",
"secret": "whsec_…48 hex characters, shown once"
}
PATCH /v1/webhooks/{id}
Change a webhook endpoint
Turning an endpoint back on forgives its failure count.
| Field | Type | Constraints | |
|---|---|---|---|
| active | boolean | optional | |
| events | string[] | optional | 1–30 items |
| label | string | optional | 0–80 chars |
{
"updated": true
}
POST /v1/webhooks/{id}/rotate
Rotate a webhook endpoint's secret
A new secret, returned once. For 24 hours every delivery is signed with both — the signature header carries two comma-separated v1= values, the new secret's first — so a receiver can switch without dropping a delivery. After that the old secret is forgotten.
{
"secret": "whsec_…shown once",
"previous_valid_for_seconds": 86400
}
GET /v1/webhooks/{id}/deliveries
List an endpoint's recent deliveries
The last fifty, newest first: event, status, attempts, the next attempt, the last error and the document. A replay names the delivery it repeats in replay_of.
{
"deliveries": [
{
"id": "dl1…",
"endpoint_id": "h4a7…",
"event": "signing.completed",
"status": "delivered",
"attempts": 1,
"next_attempt_at": "2026-10-03T14:22:09Z",
"last_error": null,
"response_status": 200,
"created_at": "2026-10-03T14:22:09Z",
"delivered_at": "2026-10-03T14:22:09Z",
"replay_of": null,
"document_id": "d41c…",
"document_title": "Liability waiver — Dana Reyes"
}
]
}
POST /v1/webhooks/deliveries/{id}/replay
Send a delivery again
Queues a delivered, failed or dead delivery again as a new delivery with the same event and body, marked as a replay of the original. One still in the queue is refused; so is a second replay while the first is queued. A replay waits like any other if the endpoint is off or paused.
{
"delivery_id": "dl2…"
}
DELETE /v1/webhooks/{id}
Remove a webhook endpoint
Needs ?reason=, which goes on the audit trail.
{
"deleted": true
}
Sign
GET /v1/sign/{token}
Read a document from a signing link
Everything the signing page needs from one token: the text and its hash, who is being asked, who else is on it, whether it is this signer's turn, and — when it will not take a signature — why, in a sentence a person can read. Returns a body even for a token that matches nothing, so a bad link and an expired one fail identically.
{
"ok": true,
"reason": null,
"document_id": "d41c…",
"title": "Liability waiver — Dana Reyes",
"body": "I, Dana Reyes, …",
"body_sha256_hex": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"document_status": "sent",
"sequential": false,
"expires_at": "2026-10-17T00:00:00Z",
"tenant_name": "Fern Hollow Stables",
"signer_id": "5f0a…",
"full_name": "Dana Reyes",
"email": "dana@example.com",
"signer_role": "signer",
"sign_order": 1,
"signer_status": "pending",
"consented_at": null,
"signed_at": null,
"template_kind": "waiver",
"grants": {},
"is_my_turn": true,
"waiting_on": null,
"signers": [
{
"full_name": "Dana Reyes",
"role": "signer",
"sign_order": 1,
"status": "pending",
"signed_at": null,
"is_me": true
}
],
"fields": [
{
"key": "initials",
"label": "Initial here",
"kind": "initials",
"required": true,
"about": null,
"supplied": false
}
]
}
POST /v1/sign/{token}/consent
Record consent to sign electronically
Must happen before the signature, and is its own event for that reason: ESIGN requires consent to precede the transaction, and a system that recorded both in the same instant could not show that it did.
| Field | Type | Constraints | |
|---|---|---|---|
| captured | object | optional |
{
"consented": true
}
POST /v1/sign/{token}/fields
Supply a required field
Initials, a checkbox, a date. A signature is refused while anything required is unsupplied, and that refusal is in the database rather than in the form — so a second interface cannot go around it.
| Field | Type | Constraints | |
|---|---|---|---|
| key | string | required | matches ^[a-z][a-z0-9_]{0,58}$ |
| value | string | required | 0–20000 chars |
| about | string | null | optional | 0–200 chars |
{
"supplied": true
}
POST /v1/sign/{token}
Sign
Typed or drawn, both equally valid: what matters is intent. captured is for a signature taken where there was no network — the server records when it received it and keeps the device's claim beside it, with the clock skew computed rather than accepted.
| Field | Type | Constraints | |
|---|---|---|---|
| method | "typed" | "drawn" | "clicked" | "uploaded" | required | |
| value | string | required | 1–400000 chars |
| captured | object | optional |
{
"event_id": "e3…"
}
POST /v1/sign/{token}/decline
Decline to sign
With a reason. A decline is a person saying no, which is not the same as still waiting — a packet containing one is blocked rather than outstanding, so nobody chases somebody who already answered.
| Field | Type | Constraints | |
|---|---|---|---|
| reason | string | required | 1–500 chars |
{
"declined": true
}
Verify
GET /v1/verify/{code}
Check a certificate by its printed code
Anonymous, because the point is that a stranger holding a printed page can check it. Answers three separate questions: whether the certificate exists, whether a document you hold matches it, and whether it still matches the record behind it. The third is a finding, never a correction.
{
"found": true,
"verification_code": "K7QP3MTV9XBD2FHR",
"issued_at": "2026-10-03T14:22:09Z",
"issuer": "Fern Hollow Stables",
"title": "Liability waiver — Dana Reyes",
"document_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
"document_matches": true,
"still_matches_record": true,
"divergence": null,
"completed_at": "2026-10-03T14:22:09Z",
"redacted_at": null,
"signers": [
{
"full_name": "Dana Reyes",
"email": "dana@example.com",
"role": "signer",
"sign_order": 1,
"status": "signed",
"consented_at": "2026-10-03T14:22:09Z",
"signed_at": "2026-10-03T14:22:09Z"
}
],
"events": [
{
"kind": "signed",
"occurred_at": "2026-10-03T14:22:09Z",
"signer": "dana@example.com",
"against_sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324"
}
]
}
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 25 agreements a month.
Get an API key