Signing requests
Create, read, remind, and withdraw — the four endpoints around the signing_request resource.
A signing request is one signer's row on a document. A document with two signers has two signing requests; with five signers, five. Each carries a status, an expiry, an audit log, and a unique signing URL the signer clicks to sign.
Create
The big one — uploads the PDF, places the fields, dispatches the emails, and returns a per-signer URL list. Multipart upload because the PDF rides as a binary form field.
Request body
Send the PDF one of two ways:
- multipart/form-data with a
filepart (the JSON fields ride as form fields), or - application/json with a
file_url— we fetch the PDF server-side (SSRF-guarded: https/http only, no private hosts, no redirects, ≤ 25 MB).
| Field | Type | Required | Description |
|---|---|---|---|
file | multipart | one of | The PDF to sign. ≤ 25 MB. |
file_url | string (JSON) | one of | URL we fetch the PDF from. Alternative to file. |
filename | string | Override the stored filename when using file_url. | |
signers | Signer[] | ✓ | 1–20 entries. See Signer object. |
placement | "anchors" | "auto_append" | "manual" | "explicit" | Default anchors with fallback to auto_append. See Placement modes. | |
fields | Field[] | Required when placement="explicit". | |
observer_emails | string[] | 0–20 CC-style recipients. | |
callback_url | string | Per-request webhook override. HMAC-signed. | |
signing_mode | "parallel" | "sequential" | Default parallel. | |
locale | "en" | "de" | Default email + UI language. | |
expires_in_days | int 1..90 | Default 14. Per-link TTL. | |
placement_assignee_email | string | Optional, for placement="manual". If set, we email them the placement link; if omitted, you just get the placementUrl back to redirect your own user. |
Field object (placement="explicit")
| Field | Type | Required | Description |
|---|---|---|---|
page | int ≥ 0 | ✓ | 0-based page index. |
x,y | 0..1 | ✓ | Position as a fraction of page width/height. |
w,h | 0..1 | ✓ | Size as a fraction of the page. |
kind | "signature" | "initial" | "date" | "text" | Default signature. | |
role | string | ✓ | Must match a signer's role. |
origin | "top-left" | "center" | Default top-left. With center, x/y is the field's centre — e.g. "centre the signature on page 3 at 70%/65%". |
// "Point us at a PDF, place a signature by its centre, send invites" — one JSON call:
{
"file_url": "https://your-dms.example.com/contracts/42.pdf",
"placement": "explicit",
"fields": [
{ "page": 2, "x": 0.70, "y": 0.65, "w": 0.18, "h": 0.06, "kind": "signature", "role": "client", "origin": "center" }
],
"signers": [
{ "role": "client", "email": "sara@acme.ch", "name": "Sara Buyer", "phone_e164": "+41791112233", "require_sms_verification": true }
]
}Signer object
| Field | Type | Required | Description |
|---|---|---|---|
email | string | ✓ | Where the invite goes. |
role | string | ✓ | [a-z][a-z0-9_-]{0,40}. Must match an anchor placeholder when placement="anchors". |
name | string | Display name. Auto-fills the typed-signature default. | |
role_label | string | { en, de } | Human-readable label shown in emails + UI. | |
locale | "en" | "de" | Override request-level locale. | |
signing_order | int ≥ 1 | Only when signing_mode="sequential". |
Response
HTTP/1.1 201 Created
Content-Type: application/json
{
"documentId": "8a1e4f9a-…",
"status": "pending",
"placement": "anchors",
"presentedSha256": "4f9a…d21c",
"anchors": { "found": 4, "fields": 4 },
"signers": [
{ "role": "tenant", "email": "tenant@example.com", "status": "pending", "signingUrl": "…", "signingRequestId": "…" },
{ "role": "landlord", "email": "owner@example.com", "status": "pending", "signingUrl": "…", "signingRequestId": "…" }
],
"observers": [],
"callback": {
"url": "https://yourapp.com/letssign/callback",
"secret": "whsec_…",
"note": "Store this secret. We do not display it again."
}
}When placement="manual" is used, the response is HTTP 202 instead of
201, includes a placementUrl + placementToken, and the per-signer
list comes back with status="awaiting_placement". Signing requests
are created when the placer hits Send — see
Placement modes.
This is the "let me place the fields myself" flow: POST a file_url with
placement="manual" and no placement_assignee_email, then redirect your
user to the returned placementUrl. They drop the fields and hit Send, and the
invites go out — no coordinates needed from your integration.
Read one signer
Status, signing URL, and the last 50 audit events for a single signer. Use this when you want a tight poll loop on one recipient without re-fetching the whole document.
{
"id": "11111111-…",
"documentId": "8a1e4f9a-…",
"signer": { "email": "tenant@example.com", "name": null, "role": "tenant", "order": null },
"status": "viewed",
"locale": "en",
"channel": "email",
"expiresAt": "2026-05-13T10:30:00Z",
"createdAt": "2026-04-29T10:30:00Z",
"signingUrl": "https://yourco.letssign.now/en/sign/abc…",
"auditEvents": [
{ "type": "email_sent", "createdAt": "2026-04-29T10:30:01Z", "meta": { "to": "…" } },
{ "type": "viewed", "createdAt": "2026-04-29T10:32:08Z", "meta": null }
]
}For a doc-level view of all signers, see GET document.
Remind
/v1/signing-requests/{id}/remindRe-send the original invite email. Allowed only while the request is
pending or viewed and not yet expired. Empty request body. The
audit trail records the API key as the actor.
200 OK { "ok": true }
409 Conflict { "error": "Cannot remind a signed request", "code": "invalid_state" }
409 Conflict { "error": "Request has expired", "code": "expired" }Withdraw
/v1/signing-requests/{id}/withdrawCancel an in-flight signing request. Status flips to withdrawn —
subsequent reminder/withdraw calls return 409, and the signing URL no
longer accepts signatures. In sequential mode, queued downstream
signers stay queued until you withdraw or resend them too.
200 OK { "ok": true }
409 Conflict { "error": "Cannot withdraw a signed request", "code": "invalid_state" }Both remind and withdraw are also available on the in-app UI under Documents → details. The API endpoints share business rules with the UI flow — same status gates, same audit-event shape.
