Digital signatures
Send a rendered PDF for digital signature: create requests with recipients/fields/anchor tags, list and poll status through the sent|viewed|signed|declined|expired|cancelled|completed lifecycle, cancel in-flight requests, and download the archived signed PDF and completion certificate (authenticated only). Covers the 20MB limit, idempotency, 402/413/502/503 errors, and signature.* outgoing webhooks.
Send a rendered PDF out for digital signature, track its lifecycle, and download the archived signed document and completion certificate. Craftkit sends the rendered PDF for digital signature, emails the recipients, hosts the signing UI, and reports status back — the signature service is handled entirely behind the Craftkit API surface.
POST /v1/signatures
GET /v1/signatures
GET /v1/signatures/:id
POST /v1/signatures/:id/cancel
GET /v1/signatures/:id/download
GET /v1/signatures/:id/certificateAll endpoints authenticate with the project API key (Authorization: Bearer $CRAFTKIT_API_KEY) and are scoped to that key's project.
Create a signature request
POST /v1/signaturesTakes a succeeded render, submits its PDF to the signature service as an atomic create-and-send signing request, and returns the persisted request at 201. The render must belong to the API key's project and have a stored PDF asset.
Quick Start
curl
curl -X POST https://api.craftkit.dev/v1/signatures \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sign-order-12345" \
-d '{
"renderId": "0193c2c3-1111-7aaa-8bbb-000000000001",
"name": "Charter handover — BK-12345",
"recipients": [
{ "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "designation": "Signer", "order": 1 }
],
"anchorTags": [
{ "anchorString": "{{sign_here}}", "type": "signature", "recipientIndex": 0, "required": true }
],
"expirationHours": 168
}'Node.js
const res = await fetch('https://api.craftkit.dev/v1/signatures', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'sign-order-12345',
},
body: JSON.stringify({
renderId: '0193c2c3-1111-7aaa-8bbb-000000000001',
name: 'Charter handover — BK-12345',
recipients: [
{ firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com', designation: 'Signer', order: 1 },
],
anchorTags: [
{ anchorString: '{{sign_here}}', type: 'signature', recipientIndex: 0, required: true },
],
expirationHours: 168,
}),
});
const signature = await res.json();Python
import os, requests
res = requests.post(
"https://api.craftkit.dev/v1/signatures",
headers={
"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}",
"Idempotency-Key": "sign-order-12345",
},
json={
"renderId": "0193c2c3-1111-7aaa-8bbb-000000000001",
"name": "Charter handover — BK-12345",
"recipients": [
{"firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "designation": "Signer", "order": 1},
],
"anchorTags": [
{"anchorString": "{{sign_here}}", "type": "signature", "recipientIndex": 0, "required": True},
],
"expirationHours": 168,
},
)
signature = res.json()Request body
| Field | Type | Description | Default |
|---|---|---|---|
renderId |
string (UUID) | The render to sign. Must be in this project and have status: "succeeded" with a stored PDF. Required. |
— |
name |
string | Human-readable name for the request (1–255 chars). | Signature request <render-id-prefix> |
recipients |
array | 1–20 recipients (see below). Required. | — |
fields |
array | Up to 200 explicit field placements by page + coordinates (see below). | — |
anchorTags |
array | Up to 200 text-anchor placements (see below). Recommended for Craftkit templates. | — |
expirationHours |
integer | Hours until the request expires (1–8760). | Provider default (168) |
At least one fields entry or anchorTags entry is required whenever any recipient is a Signer.
recipients[]
| Field | Type | Description | Default |
|---|---|---|---|
firstName |
string | Recipient first name (1–100 chars). Required. | — |
lastName |
string | Recipient last name (≤100 chars). | — |
email |
string | Recipient email (≤255 chars). Required. | — |
designation |
enum | Signer, Approver, or CC. |
Signer |
order |
integer | Signing order, ≥1. When supplied on any recipient, the supplied values must be unique and within [1, recipientCount]. |
Array position (1-based) |
fields[]
Explicit placement by page and coordinates. Coordinates are percentages of page width/height (0–100), origin top-left — the signature service's coordinate system.
| Field | Type | Description | Default |
|---|---|---|---|
recipientIndex |
integer | 0-based index into recipients. Must reference an existing recipient. Required. |
— |
type |
enum | signature, initial, text, date, checkbox, dropdown, radio_buttons, or text_area. Required. |
— |
page |
integer | 1-based page number. Required. | — |
x |
number | Horizontal position, 0–100 (% of page width). Required. | — |
y |
number | Vertical position, 0–100 (% of page height). Required. | — |
width |
number | Field width, >0 and ≤100 (% of page width). | Provider default |
height |
number | Field height, >0 and ≤100 (% of page height). | Provider default |
required |
boolean | Whether the field must be completed. | Provider default |
anchorTags[]
Placement relative to a text anchor baked into the template (e.g. add {{sign_here}} to the template body and reference it here). Unlike fields, anchor width/height are points relative to the matched text, not page percentages.
| Field | Type | Description | Default |
|---|---|---|---|
anchorString |
string | The literal text to anchor on (1–200 chars). Required. | — |
type |
enum | signature, initials, text, date, or checkbox. Note initials (plural) here vs. initial in fields — this mirrors the signature service's wire vocabulary. Required. |
— |
recipientIndex |
integer | 0-based index into recipients. Must reference an existing recipient. Required. |
— |
width |
number | Offset width in points, >0 and ≤1000. | Provider default |
height |
number | Offset height in points, >0 and ≤1000. | Provider default |
required |
boolean | Whether the field must be completed. | Provider default |
ignoreIfNotPresent |
boolean | Skip silently if the anchor text isn't found in the document. | Provider default |
Idempotency. Send an
Idempotency-Keyheader so a retried POST returns the original request instead of minting (and billing) a second signing envelope. Keys are scoped per project; a replay returns200with the original body instead of201.
Response — 201 Created
{
"id": "0193c2c3-2222-7aaa-8bbb-000000000002",
"renderId": "0193c2c3-1111-7aaa-8bbb-000000000001",
"name": "Charter handover — BK-12345",
"status": "sent",
"recipients": [
{
"id": "rcp_8e10-12ab34cd56ef",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"designation": "Signer",
"order": 1
}
],
"expirationHours": 168,
"signedDownloadUrl": null,
"certificateUrl": null,
"errorMessage": null,
"createdAt": "2026-06-05T10:00:00.000Z",
"completedAt": null
}| Field | Type | Description |
|---|---|---|
id |
string | Craftkit signature request id (UUID). Use this for status, cancel, download, and certificate. |
renderId |
string | The render that was sent for signature. |
name |
string | The request name. |
status |
string | Lifecycle status — sent on creation. See status lifecycle. |
recipients |
array | Recipient snapshot, now carrying provider-assigned ids. May also include signedAt / declinedAt once those events arrive. |
expirationHours |
integer | null | Hours until expiry, as resolved by the signature service. |
signedDownloadUrl |
string | null | Authenticated Craftkit URL to download the signed PDF (GET /v1/signatures/:id/download); null until the document is archived. |
certificateUrl |
string | null | Authenticated Craftkit URL to download the completion certificate (GET /v1/signatures/:id/certificate); null until the certificate is available. Always a Craftkit-owned URL — never a provider domain. |
errorMessage |
string | null | Human-readable error or decline/cancel reason. |
createdAt |
string | ISO-8601 timestamp. |
completedAt |
string | null | ISO-8601 timestamp set when the request completes. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_json |
Body wasn't valid JSON | Check Content-Type and JSON.stringify |
| 400 | invalid_request |
Body didn't match the schema | Inspect issues for offending fields |
| 402 | signature_credits_exhausted |
The signature service account is out of credits | Top up the signature service account |
| 404 | not_found |
No render with that id in this project | Check renderId and the key's project scope |
| 409 | conflict |
Render isn't ready for signing (not succeeded, or no PDF) |
Wait for the render to succeed, then retry |
| 413 | document_too_large |
Rendered PDF exceeds the 20MB signing limit | Reduce the document size |
| 500 | internal |
Failed to load the PDF or persist the request | Retry; if persistent, contact support |
| 502 | signature_provider_error |
The signature service rejected the create-and-send request | Inspect message; verify recipients/fields |
| 503 | signatures_unavailable |
Digital signatures are not configured on this server | Enable digital signatures, or contact support |
List signature requests
GET /v1/signaturesReturns the project's signature requests, newest first, cursor-paginated.
Quick Start
curl
curl "https://api.craftkit.dev/v1/signatures?limit=20" \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"Node.js
const res = await fetch('https://api.craftkit.dev/v1/signatures?limit=20', {
headers: { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` },
});
const { signatures, nextCursor, hasMore } = await res.json();Python
import os, requests
res = requests.get(
"https://api.craftkit.dev/v1/signatures",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
params={"limit": 20},
)
page = res.json()Query parameters
| Field | Type | Description | Default |
|---|---|---|---|
limit |
integer | Page size, 1–100. | 50 |
renderId |
string (UUID) | Filter to signature requests for a single render. | — |
cursor |
string (ISO-8601) | Keyset cursor — return rows created strictly before this timestamp. Pass the previous page's nextCursor. |
— |
Response — 200 OK
{
"signatures": [
{
"id": "0193c2c3-2222-7aaa-8bbb-000000000002",
"renderId": "0193c2c3-1111-7aaa-8bbb-000000000001",
"name": "Charter handover — BK-12345",
"status": "completed",
"recipients": [],
"expirationHours": 168,
"signedDownloadUrl": "https://api.craftkit.dev/v1/signatures/0193c2c3-2222-7aaa-8bbb-000000000002/download",
"certificateUrl": "https://api.craftkit.dev/v1/signatures/0193c2c3-2222-7aaa-8bbb-000000000002/certificate",
"errorMessage": null,
"createdAt": "2026-06-05T10:00:00.000Z",
"completedAt": "2026-06-05T11:42:08.000Z"
}
],
"nextCursor": "2026-06-05T10:00:00.000Z",
"hasMore": false
}| Field | Type | Description |
|---|---|---|
signatures |
array | Signature requests (same shape as the create response). |
nextCursor |
string | null | Pass as cursor to fetch the next page; null when there are no more rows. |
hasMore |
boolean | true when a full page was returned and more rows may exist. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_request |
Invalid query parameters | Check limit, renderId, cursor formats |
| 401 | unauthorized |
Missing/invalid/revoked key | Check the bearer token |
Get a signature request
GET /v1/signatures/:idPoll a single signature request for its current status and (once available) the signed-document URL and certificate.
Quick Start
curl
curl https://api.craftkit.dev/v1/signatures/$SIGNATURE_ID \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"Node.js
const res = await fetch(`https://api.craftkit.dev/v1/signatures/${id}`, {
headers: { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` },
});
const signature = await res.json();Python
import os, requests
res = requests.get(
f"https://api.craftkit.dev/v1/signatures/{signature_id}",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
)
signature = res.json()Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string (UUID) | The signature request id. | — |
Response — 200 OK
Same shape as the create response. Watch status, signedDownloadUrl, certificateUrl, and completedAt change as the request progresses.
Status lifecycle
status moves through these values as the signature service delivers lifecycle events:
| Status | Meaning |
|---|---|
sent |
Request created; the recipients have been emailed. |
viewed |
A recipient opened the signing UI. |
declined |
A recipient declined to sign (terminal). |
expired |
The request passed its expiration window (terminal). |
cancelled |
The request was cancelled (terminal). |
completed |
All recipients signed and the document is finalized (terminal). |
failed |
Processing failed (terminal). |
Recipient-level signing arrives as a signature.signed webhook (and per-recipient signedAt timestamps) without moving the top-level status. Once the request is completed, Craftkit archives the signed PDF to its own storage and populates signedDownloadUrl and certificateUrl.
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | unauthorized |
Missing/invalid/revoked key | Check the bearer token |
| 404 | not_found |
No signature request with that id in this project | Check the id and key's project scope |
Cancel a signature request
POST /v1/signatures/:id/cancelCancels an in-flight request with the signature service and marks it cancelled. Rejected with 409 when the request is already in a terminal state (completed, declined, expired, cancelled, or failed).
Quick Start
curl
curl -X POST https://api.craftkit.dev/v1/signatures/$SIGNATURE_ID/cancel \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer changed the terms" }'Node.js
const res = await fetch(`https://api.craftkit.dev/v1/signatures/${id}/cancel`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ reason: 'Customer changed the terms' }),
});
const signature = await res.json();Python
import os, requests
res = requests.post(
f"https://api.craftkit.dev/v1/signatures/{signature_id}/cancel",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
json={"reason": "Customer changed the terms"},
)
signature = res.json()Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string (UUID) | The signature request id. | — |
Request body
The body is optional (an empty body is accepted).
| Field | Type | Description | Default |
|---|---|---|---|
reason |
string | Cancellation reason (≤500 chars). Stored on the request as errorMessage. |
— |
Response — 200 OK
The updated signature request (same shape as the create response), now with status: "cancelled".
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_json |
Body wasn't valid JSON | Send an empty body or valid JSON |
| 400 | invalid_request |
Body didn't match the schema | reason must be a string ≤500 chars |
| 401 | unauthorized |
Missing/invalid/revoked key | Check the bearer token |
| 404 | not_found |
No signature request with that id in this project | Check the id and key's project scope |
| 409 | conflict |
Request is already terminal and can't be cancelled | Don't cancel completed/declined/expired/cancelled requests |
| 502 | signature_provider_error |
The signature service failed to cancel the request | Retry; reconcile via the status endpoint |
Download the signed PDF
GET /v1/signatures/:id/downloadStreams the archived signed PDF. The signed document is a sensitive legal artifact, so it is served only through the authenticated API (streamed from storage) — it is never reachable from a public bucket URL. Returns 409 until the document has been archived (i.e. until signedDownloadUrl is non-null).
Quick Start
curl
curl https://api.craftkit.dev/v1/signatures/$SIGNATURE_ID/download \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-o signed.pdfNode.js
const res = await fetch(`https://api.craftkit.dev/v1/signatures/${id}/download`, {
headers: { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` },
});
const pdf = Buffer.from(await res.arrayBuffer());Python
import os, requests
res = requests.get(
f"https://api.craftkit.dev/v1/signatures/{signature_id}/download",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
)
with open("signed.pdf", "wb") as f:
f.write(res.content)Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string (UUID) | The signature request id. | — |
Response — 200 OK
The signed PDF bytes (Content-Type: application/pdf), served as an attachment named <id>-signed.pdf.
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | unauthorized |
Missing/invalid/revoked key | Check the bearer token |
| 404 | not_found |
No signature request with that id in this project | Check the id and key's project scope |
| 409 | conflict |
Signed document isn't archived yet | Poll status until signedDownloadUrl is set |
Download the completion certificate
GET /v1/signatures/:id/certificateStreams the completion certificate (the audit trail PDF) for a finished signature request. Like the signed document, the certificate is served only through the authenticated API — Craftkit fetches it server-side and streams the bytes back, so the underlying provider URL never appears on the wire. Returns 409 until the certificate is available (i.e. until certificateUrl is non-null), and 502 if the document can't be retrieved upstream.
Quick Start
curl
curl https://api.craftkit.dev/v1/signatures/$SIGNATURE_ID/certificate \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-o certificate.pdfNode.js
const res = await fetch(`https://api.craftkit.dev/v1/signatures/${id}/certificate`, {
headers: { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` },
});
const pdf = Buffer.from(await res.arrayBuffer());Python
import os, requests
res = requests.get(
f"https://api.craftkit.dev/v1/signatures/{signature_id}/certificate",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
)
with open("certificate.pdf", "wb") as f:
f.write(res.content)Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string (UUID) | The signature request id. | — |
Response — 200 OK
The completion certificate bytes (Content-Type: application/pdf), served as an attachment named <id>-certificate.pdf.
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | unauthorized |
Missing/invalid/revoked key | Check the bearer token |
| 404 | not_found |
No signature request with that id in this project | Check the id and key's project scope |
| 409 | conflict |
Completion certificate isn't available yet | Poll status until certificateUrl is set |
| 502 | upstream_error |
The certificate couldn't be retrieved from the signature service | Retry; if persistent, contact support |
Signature webhook events
If your project has a webhook subscription (configured in the dashboard, see Webhooks), Craftkit emits signed outgoing webhooks to your server as the signature request changes state. These ride the same delivery contract as render webhooks: a POST of a JSON body with an event field, signed with HMAC-SHA256 of the raw body (hex, no prefix) in the x-craftkit-signature header, plus x-craftkit-event, x-craftkit-timestamp, and x-craftkit-delivery-id. Verify the signature against the subscription secret before trusting the payload, and respond 2xx promptly.
| Event | Fired when |
|---|---|
signature.sent |
The request was created and recipients were emailed. |
signature.viewed |
A recipient opened the signing UI. |
signature.signed |
A single recipient signed (per-recipient; does not move the top-level status). |
signature.completed |
All recipients signed and the document is finalized. |
signature.declined |
A recipient declined to sign. |
signature.expired |
The request passed its expiration window. |
signature.cancelled |
The request was cancelled. |
A subscription only receives the events it is subscribed to. Each payload carries the Craftkit event name plus signatureRequestId, renderId, and a provider-neutral status; subscribe and verify on your side just as you would for render.* events. The payload never includes any provider-specific event type or identifier.
{
"event": "signature.completed",
"signatureRequestId": "0193c2c3-2222-7aaa-8bbb-000000000002",
"renderId": "0193c2c3-1111-7aaa-8bbb-000000000001",
"status": "completed"
}| Field | Type | Description |
|---|---|---|
event |
string | The Craftkit event name (e.g. signature.completed). |
signatureRequestId |
string | The signature request id. |
renderId |
string | The render that was sent for signature. |
status |
string | Provider-neutral lifecycle status — one of sent, viewed, completed, declined, expired, cancelled. |
Related
- Render a template — produce the PDF you send for signature
- GET /v1/renders/:id — confirm the render succeeded first
- Webhooks — subscribe to
signature.*lifecycle events - Errors — error envelope and retry semantics
- Authentication — bearer token format