Craftkitdocs

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/certificate

All 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/signatures

Takes 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-Key header so a retried POST returns the original request instead of minting (and billing) a second signing envelope. Keys are scoped per project; a replay returns 200 with the original body instead of 201.

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/signatures

Returns 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/:id

Poll 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/cancel

Cancels 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/download

Streams 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.pdf

Node.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/certificate

Streams 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.pdf

Node.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.