Signum

Developer guide

The Signum API and webhooks

Send an agreement from your own system, track it to completion, pull the sealed PDF and its certificate, and verify a document years later — over plain HTTPS and JSON.

What you need

Programmatic access is part of the Group plan. There are three separate capabilities and Group includes all of them:

CapabilityWhat it coversPlan
API accessEverything under /v1 — creating, tracking and downloading envelopes.Group
WebhooksSigned HTTP callbacks when something happens to an envelope.Group
Verification APIPOST /v1/verify — checking a document hash against the sealed record.Group
Reads are gated too

The entitlement is checked on every request, not only on writes. If a subscription moves off Group, existing keys stop returning data and registered webhook endpoints stop delivering. Nothing is deleted — the keys and endpoints stay exactly as they were, and moving back to Group resumes delivery with no reconfiguration.

The human-facing verification page is free for everyone on every plan, and always will be. Only verifying at scale, programmatically is a Group capability.

Authentication

Every request carries an organisation API key in the x-api-key header. Keys look like esk_live_… and are scoped to one business — the key decides which organisation you are acting as, so there is no organisation parameter anywhere in the API.

# every request
curl https://api.getsignum.uk/v1/envelopes/95260817000123 \
  -H 'x-api-key: esk_live_your_key_here'

Create and revoke keys in Settings → API keys in the app. Only a hash of the key is stored, so the full value is shown once at creation and cannot be recovered — if you lose it, revoke it and issue another.

Keep it server-side

An API key can send agreements in your company's name. Never put one in a browser, a mobile app, or anything a customer can read. If a key is exposed, revoke it in Settings — revocation takes effect immediately.

Quick start

Sending is template-driven: you prepare a template once in the app — documents, fields, and a named role for each party — and the API fills in who the parties are. That way your code never has to describe where a signature box goes.

1 · Prepare a template in the app

Build an envelope as normal, then Save as template and name each role for the part it plays: Client, Landlord, Tenant 1. Note the template key shown on the template.

2 · Send it

curl -X POST https://api.getsignum.uk/v1/envelopes \
  -H 'x-api-key: esk_live_…' \
  -H 'content-type: application/json' \
  -d '{
    "template_key": "tenancy-agreement",
    "external_ref": "job-48213",
    "subject": "Tenancy agreement — 14 Riverside Court",
    "message": "Please review and sign by Friday.",
    "recipients": [
      { "role": "Landlord", "name": "A Silva",  "email": "a@example.co.uk" },
      { "role": "Tenant 1", "name": "R Bhatt",  "email": "r@example.co.uk" }
    ]
  }'

The envelope is created and sent in one call. You get back:

{
  "envelope_id": "6f2c…",
  "envelope_ref": "95260817000123",
  "status": "SENT",
  "subject": "Tenancy agreement — 14 Riverside Court",
  "expires_at": "2026-08-31T12:00:00+00:00",
  "recipients": [
    { "name": "A Silva", "email": "a@example.co.uk",
      "status": "SENT", "routing_order": 1 }
  ],
  "documents_finalized": false
}

envelope_ref is the permanent handle — 14 characters, always starting 95. Store it against your own record.

3 · Wait to be told, don't poll

Register a webhook and you will be told the moment it completes. Polling GET /v1/envelopes/{ref} works, but no faster than a signer can sign — keep it to a few times an hour if you must.

Sign on screen (embedded signing)

When the signer is already on your website — finishing a sign-up form, say — you don't have to make them go and find an email. Ask for signing_links and Signum hands back each signer's own signing link. Send their browser there; when they finish (or decline) Signum sends them back to your return_url.

curl -X POST https://api.getsignum.uk/v1/envelopes \
  -H 'x-api-key: esk_live_…' \
  -H 'content-type: application/json' \
  -d '{
    "template_key": "terms-of-service",
    "external_ref": "signup-7c41",
    "signing_links": true,
    "fields": {
      "business_name": "Acme Plumbing Ltd",
      "owner_name": "Jo Smith",
      "plan": "Starter",
      "trial_end": "2026-11-02"
    },
    "recipients": [
      { "role": "Owner", "name": "Jo Smith", "email": "jo@acme.example",
        "return_url": "https://app.example.com/signup/done" }
    ]
  }'
{
  "envelope_ref": "95261005000417",
  "status": "SENT",
  "recipients": [
    { "role": "Owner", "name": "Jo Smith", "email": "jo@acme.example",
      "status": "SENT", "routing_order": 1, "return_url_set": true,
      "signing_url": "https://getsignum.uk/sign?token=…" }
  ],
  …
}

Redirect the signer's browser (a normal top-level redirect) to signing_url.

Coming back

After they sign, the signing page sends them to return_url with three query parameters added (any you already had are kept):

https://app.example.com/signup/done?envelope_ref=95261005000417&external_ref=signup-7c41&status=signed

status is signed or declined. Without a return_url the signer sees Signum's usual confirmation page instead.

The return is not proof

Anyone can type those parameters into a browser. Use the redirect to show the right screen, but act — create the account, start the service — only on the signed envelope.completed webhook, or after checking GET /v1/envelopes/{envelope_ref} says COMPLETED.

Allowed return addresses

So a signing page on getsignum.uk can never be used to bounce people to an arbitrary site, a return_url must be https:// and its origin must be on your organisation's list in Settings → API & webhooks → Return addresses (for example https://app.example.com). Until you add one, requests with a return_url are refused with 422. A return_url at the top level of the body applies to every recipient that doesn't set its own.

The email copy

By default the signer is also emailed the invitation, so they have a record and can come back to it later; the link in the email and signing_url are the same link. Pass "send_email": false to skip the invitation (and the automatic reminders) when you are putting the signer on the page yourself — this needs "signing_links": true, or the signer would never hear about it. Everyone still receives the sealed PDF by email when the envelope completes.

A fresh link later

Signing links last until the envelope expires. If you need one again — the signer closed the tab, or a later signer's turn has come in a signing-order envelope — ask for a new one:

curl https://api.getsignum.uk/v1/envelopes/95261005000417/recipients/Owner/signing-link \
  -H 'x-api-key: esk_live_…'

{ "role": "Owner", "name": "Jo Smith", "email": "jo@acme.example",
  "status": "VIEWED", "signing_url": "https://getsignum.uk/sign?token=…",
  "expires_at": "2026-11-04T10:02:11+00:00" }

Each signer has one live link at a time, so issuing a new one retires the previous link — including the one in their email; opening it shows a message pointing them to the newest. Repeating a create call with the same external_ref and "signing_links": true also returns fresh links. Treat a signing link like a password: send it straight to that signer's browser, never log it or show it to anyone else.

The signing page is a full page of its own, built for phones and desktops; it is not designed to be shown inside an <iframe>. Redirect to it.

Pre-filled fields

Details you already know — the business name, the plan, a trial end date — can be written into the agreement for the signer, so they read them rather than type them.

1 · Give the fields a key

In the app, open Templates, choose Field keys on the template, and give each text or date field a key: lower-case letters, numbers and underscores, such as business_name or trial_end. The same key on several fields fills all of them. Tick Signer can change it if the signer should be able to correct the value; otherwise it is shown read-only.

2 · Send values in fields

fields is an object of key to value, as in the example above. Text values are a single line of up to 200 characters. Date values are written YYYY-MM-DD and appear on the document as, for example, 2 Nov 2026. A key you leave out stays empty for the signer to fill in (if it is their field).

An unknown key, a value that is not text, an over-long value or a date that isn't a real date is refused with 422 and a message naming every problem, before anything is created. Pre-filled values are recorded in the envelope's tamper-evident audit trail, printed in the sealed PDF exactly like typed values, and listed on the certificate of completion under Details filled in by the sender.

Finding a template's keys

GET /v1/templates lists each template with its roles and field keys, so your code can check what a template expects.

Folders

Folders (called projects in the app) keep a business's envelopes apart, for example test sign-ups and real ones. In the app, open Projects, create a folder and give it a folder key: lower-case letters, numbers and hyphens, such as signups-test. The key stays the same if you rename the folder.

{ "template_key": "terms-of-service", "folder": "signups-test", … }

folder on POST /v1/envelopes must be the key of an existing, active folder. An unknown or archived key is refused with 422, listing the keys that exist. Folders are never created on first use: a typo would quietly start a new folder that no folder-scoped webhook listens to, and you would never hear the envelope complete. Leave folder out to keep the envelope unfiled.

Every envelope response and every webhook payload carries "folder": {"key": "signups-test", "name": "Sign-ups (test)"}, or null when unfiled. GET /v1/envelopes?folder=signups-test lists one folder's envelopes, and GET /v1/folders lists the folders.

One webhook per folder

A webhook can listen to one folder only. Choose the folder when you add the webhook in Settings → API & webhooks. A test receiver and a live receiver can then sit side by side in the same business, each hearing only its own envelopes. A webhook with no folder hears everything, as before. Moving an envelope to another folder changes which webhook hears its later events.

Endpoints

Base URL: https://api.getsignum.uk — always this, never the execute-api hostname you may see elsewhere. That one carries a generated id which changes if the API is ever rebuilt. Everything below sits under /v1 and needs Group.

POST/v1/envelopes

Create an envelope from a template and send it immediately. Body: template_key (required), recipients (array of role, name, email and optional return_url), subject, message, external_ref, expires_in_days, reminders, and for on-screen signing signing_links, send_email, return_url and fields (see above). Returns 201, or 200 with the existing envelope when external_ref has been seen before.

GET/v1/envelopes?external_ref=job-48213

Look up by your own reference. Returns the most recent envelope with that reference, ignoring any that were voided, expired or declined. 404 if there is none. Without external_ref it lists envelopes, newest first, as {"items", "page", "page_size", "total"}. Filter with folder and status; page with page (from 0) and page_size (up to 100).

GET/v1/envelopes/{envelope_ref}

Current status, every recipient and where each one has got to, and whether the sealed documents are ready.

GET/v1/envelopes/{envelope_ref}/documents/final

Time-limited download URLs for the sealed PDFs and the certificate of completion, each with its sha256. Returns 409 while sealing is still in progress — wait for envelope.completed rather than retrying in a loop.

GET/v1/envelopes/{envelope_ref}/recipients/{role}/signing-link

A fresh signing link for the signer with that template role (URL-encode a role with spaces: Tenant%201). Retires the signer's previous link. 409 if the envelope is closed, the signer has finished, or it is not yet their turn.

GET/v1/folders

Your active folders: key (null until a key is set in the app), name, and how many envelopes are in each.

GET/v1/templates

Your templates: template_key, name, roles, and the field keys each accepts in fields with their type (TEXT or DATE).

POST/v1/envelopes/{envelope_ref}/void

Cancel an envelope that has not completed. Body: reason. Signers are told, their links stop working, and the reason is written into the audit trail.

POST/v1/envelopes/{envelope_ref}/remind

Send a reminder now to whoever has not signed, on top of any automatic schedule.

/v1/verify

Body: envelope_ref and a 64-character sha256. Answers whether that exact file is the one Signum sealed. Scoped to your own organisation — a key can only verify documents it sent.

{ "valid": true, "envelope_ref": "95260817000123",
  "status": "COMPLETED",
  "completed_at": "2026-08-20T09:14:03+00:00",
  "reason": null }

Idempotency

Sending an agreement twice is a real problem — two emails, two documents, a confused client. Pass external_ref in the body, or an Idempotency-Key header if your HTTP client prefers that; they mean the same thing.

If Signum has already created a live envelope with that reference, the call returns 200 and the existing envelope rather than 201 and a second one. Voided, expired and declined envelopes are ignored for this check, so re-sending after a decline does what you would want.

Use something stable from your own system — a job number, an invoice id. Not a timestamp.

Webhooks

Register an endpoint in Settings → Webhooks, choose which events you want, and Signum POSTs JSON to you when they happen.

Events

These are the events that are delivered. There are no others.

EventWhenIn data
envelope.completedEveryone has signed and the document is sealed. This is the one that matters.final_documents
recipient.completedOne party has finished, others may remain.recipient — name, email
envelope.declinedSomeone refused to sign. The envelope is over.declined_by, reason
envelope.voidedCancelled by the sender or through the API.reason
envelope.expiredPassed its expiry date unsigned.—

Subscribe to * to receive all of them, including any added later.

Payload

{
  "event_id": "b1e0…",
  "event_type": "envelope.completed",
  "occurred_at": "2026-08-20T09:14:03.221+00:00",
  "envelope_ref": "95260817000123",
  "external_ref": "job-48213",
  "org_slug": "acme-lettings",
  "folder": { "key": "lettings-live", "name": "Lettings" },
  "data": { /* per event, see the table */ }
}

The request also carries X-Esign-Event with the event type, so you can route without parsing the body.

Delivery and retries

Reply 2xx quickly — do the work afterwards. Anything else, or a timeout, is retried with a widening gap:

1 min · 5 min · 15 min · 1 hr · 3 hr · 6 hr · 12 hr · 24 hr

After the last attempt the delivery is marked dead and not retried again. You can see every attempt, its response code and its payload in Settings → Webhooks.

Handle duplicates

A retry can arrive after your server has already processed the first attempt — for example when your response was slow rather than failed. event_id is unique per event: record the ones you have handled and ignore repeats.

Verifying the signature

Every webhook is signed so you can prove it came from Signum. The X-Esign-Signature header looks like:

X-Esign-Signature: t=1787053243,v1=6b8c1f…

v1 is HMAC-SHA256 of the string "{t}.{raw request body}", keyed with your endpoint's signing secret, hex-encoded. Sign the raw body bytes, before any JSON parsing — re-serialising changes the bytes and the signature will not match.

# Python
import hmac, hashlib, time

def verify(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sent = parts["t"], parts["v1"]
    # Reject anything older than five minutes — a valid signature
    # replayed a week later is still a valid signature.
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sent)
Use a constant-time compare

hmac.compare_digest in Python, crypto.timingSafeEqual in Node. Comparing with == leaks how much of the signature was correct, one byte at a time.

Errors

Errors are JSON with a human-readable error.

StatusMeansWhat to do
400Something in the request is wrong — a missing template_key, a bad email, an unknown role.Read the message; it names the field. Do not retry unchanged.
401Missing or invalid API key.Check the header name is x-api-key and the key has not been revoked.
402Your plan does not include this, or the business is read-only.Check the subscription. Retrying will not help.
403The key is valid but not for this organisation's data.Use the key issued to the business that owns the envelope.
404No such envelope, or not yours.Check the envelope_ref.
422The request is well-formed but can't be used — an unknown field key, a bad date, a return_url that isn't on your allowed list.Read the message; it lists every problem. Do not retry unchanged.
409Right request, wrong moment — usually the sealed documents are not ready.Wait for envelope.completed.
5xxOur fault.Retry with backoff. Creation is safe to retry if you sent an external_ref.

Notes and limits

  • Envelope references are 95 followed by 12 digits — YYMMDD and a sequence. Always 14 characters.
  • Download URLs expire. The links from /documents/final are time-limited and generated per request. Fetch the file, don't store the URL.
  • Read-only businesses can still be read through the API, but nothing can be created, sent or voided until the account is active again.
  • Timestamps are ISO 8601 with an offset, always UTC.
  • Documents are PDF only today.
  • Sealing is asynchronous. An envelope reaches COMPLETED a moment before its sealed PDF exists. documents_finalized tells you which side of that you are on.
Something missing?

If you need an endpoint that is not here, raise a ticket from Support in the app and say what you are building. The API grows from real integrations rather than guesses.