Documentation menu

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

Scope signing:read

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.

Returns

{
  "tenant_id": "3a1f…",
  "name": "Fern Hollow Stables",
  "slug": "fern-hollow",
  "key_label": "Booking system",
  "scopes": [
    "signing:read",
    "signing:write"
  ]
}

Templates

GET /v1/templates

Scope signing:read

List templates

Every version of every template, newest first, with whether each is published and how many documents were prepared from it.

Returns

{
  "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

Scope signing:templates · takes an Idempotency-Key (templates.create)

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.

FieldTypeConstraints
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

Returns

{
  "template_id": "8c2e…"
}

Documents

GET /v1/documents

Scope signing:read

List documents

Filter with status (a state, or open for draft and sent) and q (title, subject or signer).

Returns

{
  "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}

Scope signing:read

Show one document

The document, its signers, and the whole event trail with the text hash each event was made against.

Returns

{
  "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

Scope signing:write · takes an Idempotency-Key (documents.prepare)

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.

FieldTypeConstraints
template_code string required
title string optional 0–200 chars
values object optional
expires_at string (date-time) | null optional
sequential boolean optional

Returns

{
  "document_id": "d41c…"
}

POST /v1/documents/{id}/send

Scope signing:write · takes an Idempotency-Key (documents.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.

FieldTypeConstraints
signers object[] required 1–50 items

Returns

{
  "links": [
    {
      "signer_id": "5f0a…",
      "email": "dana@example.com",
      "token": "3f9c…64 hex characters"
    }
  ]
}

POST /v1/documents/{id}/void

Scope signing:write · takes an Idempotency-Key (documents.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.

FieldTypeConstraints
reason string required 1–500 chars

Returns

{
  "voided": true
}

GET /v1/documents/{id}/certificate

Scope signing:read

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.

Returns

{
  "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

Scope signing:read

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.

Returns

{
  "filename": "certificate-K7QP3MTV9XBD2FHR.pdf",
  "content_type": "application/pdf",
  "seal": {
    "commonName": "SignSealer",
    "publiclyTrusted": false,
    "timestamped": true
  },
  "bytes_base64": "JVBERi0xLjcK…"
}

GET /v1/documents/{id}/bundle

Scope signing:read

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.

Returns

{
  "filename": "evidence-bundle-K7QP3MTV9XBD2FHR.pdf",
  "content_type": "application/pdf",
  "seal": {
    "commonName": "SignSealer",
    "publiclyTrusted": false,
    "timestamped": true
  },
  "bytes_base64": "JVBERi0xLjcK…"
}

Workflows

GET /v1/workflows

Scope signing:read

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.

Returns

{
  "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

Scope signing:workflows · takes an Idempotency-Key (workflows.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.

FieldTypeConstraints
email 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

Returns

{
  "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

Scope signing: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.

FieldTypeConstraints
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

Returns

{
  "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}

Scope signing:read

Show a packet

Who owes what, where each one has got to, and a readiness state a door lock can wait on.

Returns

{
  "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

Scope signing:subjects · takes an Idempotency-Key (packets.start_item)

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.

Returns

{
  "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

Scope signing:webhooks

List webhook endpoints

Returns

{
  "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

Scope signing:webhooks · takes an Idempotency-Key (webhooks.add)

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.

FieldTypeConstraints
url string (uri) required matches ^https://
events string[] optional 1–30 items
label string optional 0–80 chars

Returns

{
  "id": "h4a7…",
  "secret": "whsec_…48 hex characters, shown once"
}

PATCH /v1/webhooks/{id}

Scope signing:webhooks

Change a webhook endpoint

Turning an endpoint back on forgives its failure count.

FieldTypeConstraints
active boolean optional
events string[] optional 1–30 items
label string optional 0–80 chars

Returns

{
  "updated": true
}

POST /v1/webhooks/{id}/rotate

Scope signing:webhooks

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.

Returns

{
  "secret": "whsec_…shown once",
  "previous_valid_for_seconds": 86400
}

GET /v1/webhooks/{id}/deliveries

Scope signing:webhooks

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.

Returns

{
  "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

Scope signing:webhooks

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.

Returns

{
  "delivery_id": "dl2…"
}

DELETE /v1/webhooks/{id}

Scope signing:webhooks

Remove a webhook endpoint

Needs ?reason=, which goes on the audit trail.

Returns

{
  "deleted": true
}

Sign

GET /v1/sign/{token}

Scope

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.

Returns

{
  "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

Scope

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.

FieldTypeConstraints
captured object optional

Returns

{
  "consented": true
}

POST /v1/sign/{token}/fields

Scope

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.

FieldTypeConstraints
key string required matches ^[a-z][a-z0-9_]{0,58}$
value string required 0–20000 chars
about string | null optional 0–200 chars

Returns

{
  "supplied": true
}

POST /v1/sign/{token}

Scope

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.

FieldTypeConstraints
method "typed" | "drawn" | "clicked" | "uploaded" required
value string required 1–400000 chars
captured object optional

Returns

{
  "event_id": "e3…"
}

POST /v1/sign/{token}/decline

Scope

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.

FieldTypeConstraints
reason string required 1–500 chars

Returns

{
  "declined": true
}

Verify

GET /v1/verify/{code}

Scope

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.

Returns

{
  "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