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

Branding

GET /v1/branding

Scope signing:read

The brand the account shows its signers

The business's name, colours, whether it has logos on file (fetch them at /brand/{tenant_id}/logo.png and logo-dark.png) and the host its signing links use. SignSealer's own until the business has the white-label add-on and has set its brand.

Returns

{
  "businessName": "Fern Hollow Stables",
  "whiteLabelEnabled": true,
  "primaryColor": "#2f5d3a",
  "accentColor": "#c98b2b",
  "backgroundColor": "#f6f4ee",
  "textColor": "#1f2a22",
  "buttonTextColor": "#ffffff",
  "hasLogo": true,
  "hasDarkLogo": true,
  "signingHost": "fern-hollow.signsealer.com",
  "footerText": "Questions? Call the barn.",
  "tenantId": "3a1f…",
  "logoUrl": "https://api.signsealer.com/brand/3a1f…/logo.png",
  "logoDarkUrl": "https://api.signsealer.com/brand/3a1f…/logo-dark.png"
}

Sms-consents

POST /v1/sms-consents

Scope signing:write · takes an Idempotency-Key (sms_consents.record)

Record consent to text a number

For a number you took somewhere SignSealer is not: your booking form, an order, the counter. Send the exact wording that was in front of the person, because that is the record a carrier asks for and it is kept verbatim. It has to say that replying STOP opts out, that HELP gets help, that message and data rates may apply, and that message frequency varies; a disclosure missing any of those is refused with 422 naming which, and nothing is recorded. One call covers the link, the reminder and the confirmation, because somebody who agreed to those agreed to all three. A number that has replied STOP cannot be re-consented by you — they have to opt in again themselves.

FieldTypeConstraints
phone string required 7–20 chars
obtained_via string required 3–200 chars
wording string required 20–2000 chars

Returns

{
  "phone": "+15035550142",
  "purposes": [
    "signing.link",
    "signing.reminder",
    "signing.completed"
  ]
}

GET /v1/sms-consents

Scope signing:read

Whether a number may be texted

Pass phone. Answers for that one number — so an integration can decide whether to put the tick box in front of somebody again. It deliberately does not list every number the account holds. may_text is what the engine will actually do rather than what one of its checks says: consent has to be live *and* the number must not be suppressed, because a number that hard-bounced keeps its consent and still cannot be reached. When it is false, why_not says which of revoked, suppressed or no_consent.

Returns

{
  "phone": "+15035550142",
  "may_text": true,
  "why_not": null,
  "obtained_via": "booking form, checkbox beside the phone field",
  "obtained_at": "2026-10-03T14:22:09Z"
}

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. fields says what the document asks for besides a signature — a date of birth, an address, one of a list — each required unless it says otherwise, and each checked when a signer supplies it. Marking one sensitive masks the answer wherever staff read it back; it is not masked on the sealed PDF, because the PDF is the document that was 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
fields object[] optional 0–50 items

Returns

{
  "template_id": "8c2e…"
}

GET /v1/templates/{id}/pdf

Scope signing:read

Whether a template is an uploaded PDF

has_pdf says whether the template is a PDF signed where it is rather than text. If it is: page_count is what the file has, drawn is how many pages have a picture a signer can read, and ready is whether those agree — a PDF template cannot be published until they do. sha256 is the fingerprint of the file itself, which is the fingerprint the template's body names and the certificate commits to. Uploading the file is a dashboard step; see /docs/pdf.

Returns

{
  "has_pdf": true,
  "page_count": 4,
  "drawn": 4,
  "ready": true,
  "pages": [
    {
      "n": 1,
      "width": 612,
      "height": 792,
      "rotation": 0
    }
  ],
  "bytes": 284113,
  "sha256": "e362cc76dae17bbe2a9fc551d9b795fa4e0f77fb488eb748378a125b11f36324",
  "original_name": "tenancy.pdf",
  "uploaded_at": "2026-10-03T14:22:09Z"
}

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",
    "selfIssued": true,
    "trust": "unverified",
    "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",
    "selfIssued": true,
    "trust": "unverified",
    "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
    }
  ]
}

GET /v1/workflows/{id}/qr

Scope signing:read

The QR code for a workflow's public form

The public address of a workflow, drawn as a QR code: an SVG to put on a page and a PNG (1024px, base64) to print. Anybody who scans it opens the form on their own phone and signs there. 404 for a workflow with no public address.

Returns

{
  "workflow_id": "w9b1…",
  "name": "Kayak rental waiver",
  "url": "https://signsealer.com/f/harbour-rentals/waiver",
  "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 33 33\" …>…</svg>",
  "png_base64": "iVBORw0KGgo…"
}

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…"
    }
  ]
}

Devices

POST /v1/devices/pair

Scope

Trade a pairing code for a device credential

The code is shown once on the fleet screen, lives fifteen minutes and is spent on first use. What comes back is an API key scoped to this device alone and the workflow it has been given, if it has one. Every refusal answers identically — a code that is wrong, expired, spent or belongs to a stopped device all read the same, because telling them apart tells a guesser whether they are warm.

FieldTypeConstraints
code string required 8–32 chars
platform "ios" | "android" | "other" optional
app_version string optional 0–40 chars
os_version string optional 0–40 chars

Returns

{
  "device_id": "de91…",
  "name": "Front counter iPad",
  "key": "mk_live_9f2a…",
  "workflow_id": "wf3c…",
  "workflow_name": "River float waiver"
}

POST /v1/devices/heartbeat

Scope signing:device

Say the device is still here, and read back what it should be running

The device does not say which device it is — the credential it called with does. So a tablet cannot report on another's behalf, and a heartbeat from a revoked credential resolves to no device at all. The answer carries the device's name and assigned workflow, so a change made on the fleet screen reaches the tablet without re-pairing.

FieldTypeConstraints
app_version string optional 0–40 chars
os_version string optional 0–40 chars

Returns

{
  "device_id": "de91…",
  "name": "Front counter iPad",
  "workflow_id": "wf3c…",
  "workflow_name": "River float waiver"
}

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 and releases what it held. A paused endpoint keeps receiving events — they wait rather than being lost (00289) — so the answer says how many are waiting to go out, and how many were dropped because the hold was full while it was paused.

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. When the template is an uploaded PDF, pages lists its pages and each field may carry a place saying where on one it belongs; fetch a page as an image from /s/{token}/page?n=1. 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,
  "consent_text": "By continuing you agree to sign this document electronically and to receive a copy by email. …",
  "consent_version": "consent/1",
  "intent_text": "By signing below I confirm that I have read this document, …",
  "intent_version": "intent/1",
  "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}/code

Scope

Send the signer a one-time code

Only when the business asks signers for a code (Settings → Signing) and a link was delivered to this signer. The code goes to the address or number the link went to; the answer says where, masked. One a minute, five an hour. Not an identity check: it proves control of that inbox or phone at the moment of signing.

Returns

{
  "channel": "email",
  "sent_to": "d•••@example.com",
  "resend_after": "2026-10-03T14:22:09Z"
}

POST /v1/sign/{token}/verify

Scope

Confirm the one-time code

A wrong code is answered with ok false and the tries left rather than an error; after five wrong tries the code is dead and a new one must be sent. A confirmed code is a verified event on the trail and the certificate says email_otp or sms_otp.

FieldTypeConstraints
code string required 4–12 chars

Returns

{
  "ok": 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 somebody holding the document 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. With the code alone you get the issuer, title, dates, fingerprint and how many signed; pass ?sha256= of the document you hold and, when it matches, the signers (never their email addresses) and the trail.

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,
  "signer_count": 1,
  "signers": [
    {
      "full_name": "Dana Reyes",
      "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