What you need
Programmatic access is part of the Group plan. There are three separate capabilities and Group includes all of them:
| Capability | What it covers | Plan |
|---|---|---|
| API access | Everything under /v1 — creating, tracking and downloading envelopes. | Group |
| Webhooks | Signed HTTP callbacks when something happens to an envelope. | Group |
| Verification API | POST /v1/verify — checking a document hash against the sealed record. | Group |
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.
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.
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.
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.
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).
Current status, every recipient and where each one has got to, and whether the sealed documents are ready.
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.
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.
Your active folders: key (null until a key is set in the
app), name, and how many envelopes are in each.
Your templates: template_key, name, roles, and the
field keys each accepts in fields with their type
(TEXT or DATE).
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.
Send a reminder now to whoever has not signed, on top of any automatic schedule.
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.
| Event | When | In data |
|---|---|---|
envelope.completed | Everyone has signed and the document is sealed. This is the one that matters. | final_documents |
recipient.completed | One party has finished, others may remain. | recipient — name, email |
envelope.declined | Someone refused to sign. The envelope is over. | declined_by, reason |
envelope.voided | Cancelled by the sender or through the API. | reason |
envelope.expired | Passed 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.
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)
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.
| Status | Means | What to do |
|---|---|---|
400 | Something 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. |
401 | Missing or invalid API key. | Check the header name is x-api-key and the key has not been revoked. |
402 | Your plan does not include this, or the business is read-only. | Check the subscription. Retrying will not help. |
403 | The key is valid but not for this organisation's data. | Use the key issued to the business that owns the envelope. |
404 | No such envelope, or not yours. | Check the envelope_ref. |
422 | The 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. |
409 | Right request, wrong moment — usually the sealed documents are not ready. | Wait for envelope.completed. |
5xx | Our fault. | Retry with backoff. Creation is safe to retry if you sent an external_ref. |
Notes and limits
- Envelope references are
95followed by 12 digits —YYMMDDand a sequence. Always 14 characters. - Download URLs expire. The links from
/documents/finalare 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
COMPLETEDa moment before its sealed PDF exists.documents_finalizedtells you which side of that you are on.
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.