API Reference

Complete reference for the zSign REST API. Send documents, manage signing sessions, and integrate webhooks.

Prefer a terminal workflow? Validate, send, track, and test webhooks with the zSign CLI.

Base URL

https://zsign.io/api

Authentication

Bearer YOUR_API_KEY

Health & Status

API status and health check endpoints

GET

Check API Status

/

Verify the API is running.

Request

curl https://zsign.io/api

Response 200

API is running

{
"message": "zSign API is running"
}
GET

Health Check

/health

Get detailed health status of the API.

Request

curl https://zsign.io/api/health

Response 200

Health status

{
"status": "healthy"
}

Documents

Upload, send, and manage documents

GET

List Documents

/api/documents
Auth required

Retrieve documents with optional filtering.

Query Parameters

NameTypeDescription
offsetinteger= 0Pagination offset
limitinteger= 20Items per page
statusstringFilter by status
recipientstringFilter by recipient email

Request

curl -X GET "https://zsign.io/api/documents?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

List of documents

{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Contract.pdf",
"status": "pending",
"created_at": "2024-01-15T10:30:00Z"
}
],
"total": 1,
"offset": 0,
"limit": 20
}
POST

Upload and Send Document

/api/documents/send
Auth required

Upload a PDF and send it for signing in one request. PDFs must include field annotations using {type:party:name} syntax (e.g., {signature:signer}, {text:signer:name}). The party must exactly match a recipient's "role" — matching is case-sensitive, so {signature:signer} pairs with "role": "signer", not "Signer". Add * after type for required fields: {type*:party:name}. Supported types: signature, text, date, initials, radio (radio is four parts and visible-text only — see notes below).

Request Body

Content-Type: multipart/form-data

Fields

NameTypeDescription
filerequiredfilePDF file to upload
recipientsrequiredstringJSON array of recipients
namestringDocument name
send_invitestring ("true"/"false")= trueOmitting it is unchanged behavior. Set false when you deliver the signing link yourself: suppresses zSign's invite email AND reminders for this envelope. signing_urls in the response is unchanged, and completion emails are still sent. Sent as a string (this endpoint is multipart); an unrecognized value 422s.
send_completion_emailstring ("true"/"false")= trueIndependent of send_invite. Omitting it is unchanged behavior. Set false when you deliver the completed document to your signers yourself: suppresses zSign's "fully signed" email to every signer for this envelope. Does not affect the notification the envelope owner receives. Sent as a string (this endpoint is multipart); an unrecognized value 422s.
Example
[
{
"email": "john@example.com",
"name": "John Doe",
"role": "signer"
}
]

Request

curl -X POST https://zsign.io/api/documents/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@contract.pdf" \
-F 'recipients=[{"email":"john@example.com","name":"John Doe","role":"signer"}]'

Response 201

Document sent successfully

{
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "660e8400-e29b-41d4-a716-446655440000",
"signing_urls": [
{
"recipient_email": "john@example.com",
"signing_url": "https://zsign.io/sign/eyJ..."
}
]
}
### PDF Field Annotation Requirements **Sample LLM prompt to format PDF documents with correct annotation syntax:** ``` Create a fillable PDF document with embedded form field annotations. The form fields must follow these specifications: ## Field Naming Syntax: {type:party:name} Format: `{type:party:name}` or `{type*:party:name}` for required fields **Field Types:** - `signature` - Signature field - `text` - Text input field - `date` - Date field - `initials` - Initials field - `radio` - One button of an exclusive radio set (four parts: `{radio:party:group:option}`, see below) **Components:** 1. **Type**: text, date, signature, initials, or radio 2. **Party**: The role/party who fills it (e.g., signer, borrower, employee, manager, landlord, tenant). This must exactly match the `role` you send for that recipient. Matching is **case-sensitive**: `{signature:signer}` is assigned to a recipient with `"role": "signer"`, and will NOT match `"Signer"`. A party that matches no recipient's role leaves the field assigned to nobody — the document still sends, but the signer sees nothing to fill in. 3. **Name**: Field identifier (e.g., name, email, address, company_name) **Required Fields:** Add `*` after the type to make a field required: - `{text*:signer:name}` - Required text field - `{signature*:signer}` - Required signature **Examples:** - `{text:signer:full_name}` - `{text*:signer:email}` - `{date:signer:birthdate}` - `{signature:signer}` - `{initials:signer}` - `{text*:borrower:company_name}` - `{signature*:landlord}` ## Radio Fields Radio takes FOUR parts instead of three: `{radio:party:group:option}` / `{radio*:party:group:option}`. Every tag sharing a `party` and `group` forms one exclusive set — the signer picks exactly one, and the chosen `option` is the value reported back. `*` marks the whole **set** required, not the individual button (mixed markers within a set are legal and mean required). `group` is letters/digits/underscores; `option` may also contain spaces and hyphens, so `Option 1` is legal. ``` {radio*:client:term_sheet_option:Option 1} {radio*:client:term_sheet_option:Option 2} {radio*:client:term_sheet_option:Option 3} ``` **Radio tags must be visible text in the PDF body.** Unlike the other types, a radio tag cannot be the name of a PDF form field — the form-field name grammar accepts neither a fourth segment nor a space. ## Critical Technical Requirements This section applies to signature/text/date/initials only — radio is visible-text only and is never set as a widget annotation name. The field name MUST be set directly on each widget annotation's `/T` attribute, NOT only in a parent AcroForm field object. Many PDF signing services read the `/T` value directly from page annotations. **Each annotation must have:** - `/Type`: `/Annot` - `/Subtype`: `/Widget` - `/FT`: `/Tx` (for text fields) - `/T`: The field name in `{type:party:name}` format (THIS IS CRITICAL) - `/F`: `4` (print flag) - `/Rect`: `[left, bottom, right, top]` coordinates ``` **Sample PDF:** [Download simple_contract_1.pdf](https://storage.googleapis.com/zsign-public/simple_contract_1.pdf)
GET

Get Submitted Field Values

/api/v1/documents/{document_id}/fields
Auth required

Every field on a document, with its submitted value once the signer has answered it — the way to read back which radio option a signer chose, or what they typed into a text field. Accepts either the original document id or the completed document id (the one the document.completed webhook carries). Unanswered optional fields report "value": null, not an omitted key. Field values can carry personal data, so this route is API-key/owner scoped only.

Path Parameters

NameTypeDescription
document_idrequiredstringEither the original document id (from POST /api/v1/documents/send) or the completed document id

Request

curl -X GET "https://zsign.io/api/v1/documents/{document_id}/fields" \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

Every field defined on the document, with its submitted value

{
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"session_id": "660e8400-e29b-41d4-a716-446655440000",
"fields": [
{
"id": "client:term_sheet_option",
"type": "radio",
"party": "client",
"name": "term_sheet_option",
"group": "term_sheet_option",
"required": true,
"page": 3,
"value": "Option 2",
"submitted_at": "2026-08-19T14:32:00+00:00"
},
{
"id": "client:full_name",
"type": "text",
"party": "client",
"name": "full_name",
"required": true,
"page": 1,
"value": null,
"submitted_at": null
},
{
"id": "client:signature",
"type": "signature",
"party": "client",
"name": "signature",
"required": true,
"page": 1,
"status": "pending",
"signed_at": null
}
]
}
`id` is the stable `"{party}:{name}"` key — the same string used in the signing-session submit payload and in `field_assignments`. A radio group appears once here (keyed by `party:group`, with an extra `group` key) no matter how many buttons it has. `signature`/`initials` entries report `status` (`pending`/`signed`) and `signed_at` instead of `value`/`submitted_at`. A `document_id` you don't own 404s — identical to one that doesn't exist, so this can't be used to probe which ids exist.

Signing Sessions

Create and manage signing sessions

POST

Create Session

/api/sessions
Auth required

Create a new signing session.

Request Body

Content-Type: application/json

Fields

NameTypeDescription
document_idrequiredUUIDDocument to sign
document_typerequiredstringtemplate or one_off
recipientsrequiredarrayList of recipients
Example
{
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"document_type": "one_off",
"recipients": [
{
"name": "John Doe",
"email": "john@example.com",
"role": "signer",
"signing_order": 1
}
]
}

Request

curl -X POST https://zsign.io/api/sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"document_id": "550e8400-e29b-41d4-a716-446655440000",
"document_type": "one_off",
"recipients": [{
"name": "John Doe",
"email": "john@example.com",
"role": "signer",
"signing_order": 1
}]
}'

Response 201

Session created

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"expires_at": "2024-02-14T10:30:00Z",
"recipients": [
{
"recipient_id": "770e8400-e29b-41d4-a716-446655440000",
"signing_url": "https://zsign.io/sign/eyJ..."
}
]
}
GET

Get Session Status

/api/sessions/{session_id}/status
Auth required

Get lightweight session status.

Path Parameters

NameTypeDescription
session_idrequiredUUIDSession identifier

Request

curl -X GET "https://zsign.io/api/sessions/{session_id}/status" \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

Session status

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "in_progress",
"progress": {
"completed": 1,
"total": 2,
"percentage": 50
}
}
POST

Void Envelope

/api/sessions/{session_id}/void
Auth required

Terminate a live envelope. Signing links stop working immediately and pending recipients are emailed. The send credit is refunded only if no recipient has viewed, signed, or declined the envelope (declining does not require a prior view, so it can block the refund on its own) and no access has otherwise been recorded against it. Idempotent: voiding an already-voided envelope returns 200, not an error. The API-key equivalent is POST /api/v1/documents/{document_id}/void, which voids the newest session for a document. PATCH /api/sessions/{session_id}/cancel is a deprecated alias for this endpoint.

Path Parameters

NameTypeDescription
session_idrequiredUUIDSession identifier

Request Body

Content-Type: application/json

Fields

NameTypeDescription
reasonstringFree text explaining the void, max 1000 characters
Example
{
"reason": "Contract terms changed"
}

Request

curl -X POST "https://zsign.io/api/sessions/{session_id}/void" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Contract terms changed"}'

Response 200

Envelope voided

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"document_id": "660e8400-e29b-41d4-a716-446655440000",
"status": "voided",
"voided_at": "2024-01-15T10:30:00Z",
"reason": "Contract terms changed",
"recipients_to_notify": 1,
"credit_refunded": true,
"credit_refund_skipped": null
}
**`credit_refund_skipped`:** `null` when refunded; otherwise one of `already_viewed`, `no_charge_found`, `already_refunded`, `already_voided`, `not_voided`. **Errors:** `404` if the envelope does not exist or is not owned by the caller (indistinguishable, to avoid leaking existence of other accounts' data). `409` `{"detail": {"error": "envelope_not_active", "status": "<current_status>"}}` if the envelope already reached a different terminal state (`completed`, `declined`, `expired`). `422` if `reason` exceeds 1000 characters.

Webhook Event: document.voided

Webhooks

Configure webhook endpoints for real-time notifications. The document.completed payload carries a fields array — every submitted field value, in the same shape as GET /api/v1/documents/{document_id}/fields — additive on top of the existing payload keys.

POST

Create Webhook

/api/webhooks
Auth required

Create or replace webhook configuration.

Request Body

Content-Type: application/json

Fields

NameTypeDescription
urlrequiredstringWebhook endpoint URL (HTTPS)
eventsrequiredarrayList of events to subscribe to
Example
{
"url": "https://example.com/webhook",
"events": [
"document.signed",
"document.completed"
]
}

Request

curl -X POST https://zsign.io/api/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhook",
"events": ["document.signed", "document.completed"]
}'

Response 201

Webhook created

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://example.com/webhook",
"events": [
"document.signed",
"document.completed"
],
"enabled": true,
"secret": "whsec_..."
}

White-Label Branding

Brand the signer-facing experience, subscribe to White-label ($49/month, 100 envelopes included), manage a custom sender domain, and configure embed origins

GET

Get Branding Settings

/api/branding
Auth required

Retrieve the current account's branding configuration.

Request

curl -H "Authorization: Bearer YOUR_API_KEY" https://zsign.io/api/branding

Response 200

Branding settings

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "660e8400-e29b-41d4-a716-446655440000",
"company_name": "Acme Corp",
"logo_url": "https://storage.googleapis.com/zsign-public/branding/.../logo.png",
"primary_color": "#2563eb",
"custom_domain": null,
"custom_domain_verified": false,
"custom_domain_verified_at": null,
"custom_domain_target": "zsign-frontend.onrender.com",
"custom_domain_a_record": "216.24.57.1",
"badge_removed": false,
"embed_origins": [
"https://app.acme.com"
],
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
PUT

Update Branding Settings

/api/branding
Auth required

Update company name, logo URL, and accent color shown on signing pages, emails, and the Certificate of Completion.

Request Body

Content-Type: application/json

Fields

NameTypeDescription
company_namestringDisplayed on signing pages, emails, and the certificate
logo_urlstringPublic logo URL (or use POST /api/branding/logo to upload one)
primary_colorstringHex color, e.g. #2563eb, drives the signing page accent
Example
{
"company_name": "Acme Corp",
"primary_color": "#ff6600"
}

Request

curl -X PUT https://zsign.io/api/branding \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"company_name": "Acme Corp", "primary_color": "#ff6600"}'

Response 200

Updated branding settings

{
"company_name": "Acme Corp",
"primary_color": "#ff6600"
}
POST

Start White-Label Checkout

/api/branding/white-label/checkout
Auth required

Start a $49/month White-label subscription via Stripe Checkout (100 envelopes included each month). Returns 409 if the account already has an active subscription.

Request

curl -X POST https://zsign.io/api/branding/white-label/checkout \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

Stripe Checkout session URL

{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
POST

Open Billing Portal

/api/branding/white-label/portal
Auth required

Open the Stripe Billing Portal to manage or cancel the White-label subscription. Returns 400 if no subscription is on file.

Request

curl -X POST https://zsign.io/api/branding/white-label/portal \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

Stripe Billing Portal session URL

{
"portal_url": "https://billing.stripe.com/p/session/..."
}
PUT

Set Sender Domain

/api/branding/sender-domain
Auth required

Register a custom domain for outbound invite and completion emails via Resend. Requires an active White-label subscription (403 if not subscribed). Returns 409 if the domain is already claimed by another account.

Request Body

Content-Type: application/json

Fields

NameTypeDescription
domainrequiredstringDomain to send from, e.g. mail.yourcompany.com
Example
{
"domain": "mail.yourcompany.com"
}

Request

curl -X PUT https://zsign.io/api/branding/sender-domain \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "mail.yourcompany.com"}'

Response 200

Sender domain registered with Resend, not yet verified

{
"sender_domain": "mail.yourcompany.com",
"sender_domain_verified": false,
"records": [
{
"record": "TXT",
"name": "mail.yourcompany.com",
"value": "resend-verify=..."
}
]
}
Unlike the two endpoints below, this one requires an active subscription (`active`, `trialing`, or `past_due`). Switching to a different domain removes the previous Resend registration.
POST

Verify Sender Domain

/api/branding/sender-domain/verify
Auth required

Ask Resend to re-check DNS for the account's configured sender domain and report status. Not gated by subscription status, so an account whose subscription has lapsed can still confirm its DNS records.

Request

curl -X POST https://zsign.io/api/branding/sender-domain/verify \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

Current verification status and DNS records

{
"sender_domain": "mail.yourcompany.com",
"sender_domain_verified": true,
"records": [
{
"record": "TXT",
"name": "mail.yourcompany.com",
"value": "resend-verify=...",
"status": "verified"
}
]
}
DELETE

Remove Sender Domain

/api/branding/sender-domain
Auth required

Remove the configured sender domain and deregister it from Resend. Not gated by subscription status, so an account whose subscription has lapsed can still clean up its DNS.

Request

curl -X DELETE https://zsign.io/api/branding/sender-domain \
-H "Authorization: Bearer YOUR_API_KEY"

Response 200

Sender domain removed

{
"success": true
}
PUT

Set Embed Origins

/api/branding/embed-origins
Auth required

Configure the exact origins allowed to embed this account's signing pages in an iframe (default deny -- an empty list blocks all framing). Exact match only, https required except http://localhost[:port], no paths or wildcards, max 10.

Request Body

Content-Type: application/json

Fields

NameTypeDescription
originsrequiredarrayList of allowed origins, e.g. ["https://app.yourcompany.com"]
Example
{
"origins": [
"https://app.yourcompany.com"
]
}

Request

curl -X PUT https://zsign.io/api/branding/embed-origins \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"origins": ["https://app.yourcompany.com"]}'

Response 200

Updated branding settings including the normalized embed_origins list

{
"embed_origins": [
"https://app.yourcompany.com"
]
}