ZipZign API
Generate hosted PDFs, collect e-signatures, and accept payments — all from a single POST request.
From zero to a signed document in under 5 minutes. You'll need an API key from your dashboard.
# Readable document (simplest case) curl -X POST https://zipzign.com/api/documents -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{ "type": "readable", "html": "<h1>Hello World</h1><p>My first ZipZign document.</p>" }'
Response includes a url field — share it with anyone, no login required.
# Signable document — signer gets an email with a signing link curl -X POST https://zipzign.com/api/documents -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{ "type": "signable", "html": "<h1>NDA</h1><p>This Non-Disclosure Agreement...</p>", "signers": [ { "email": "alice@example.com", "name": "Alice" } ] }'
Alice receives an email with a unique link. She signs in-browser — no account needed. You get the final PDF by email and webhook.
# Payable document — payer sees a Stripe form embedded in the PDF view curl -X POST https://zipzign.com/api/documents -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{ "type": "payable", "html": "<h1>Invoice #1001</h1><p>Logo design services...</p>", "amount": 150000, "currency": "usd", "payer": { "email": "client@example.com", "name": "Bob" } }'
amount is in cents (150000 = $1,500.00). The PDF is stamped PAID automatically on completion.
"sandbox": true to any request. Sandbox documents skip real emails, real Stripe charges, and don't count against your quota. They expire after 24 hours.
Protected endpoints require an API key passed as a Bearer token in the Authorization header.
# All protected requests
Authorization: Bearer <YOUR_API_KEY>
401 Unauthorized. Contact the API owner to obtain a key.
ZipZign supports multiple active API keys simultaneously. To rotate a key without interruption:
- Generate a new API key in the dashboard.
- Update your integration to use the new key and verify it works.
- Delete the old key from the dashboard.
Both keys are valid simultaneously during the transition window — no downtime required.
A document moves through the following statuses over its lifecycle:
| Status | Description |
|---|---|
| created | Initial state, PDF being generated. |
| hosted | Readable document is live and accessible. |
| viewed | Document was opened by a recipient for the first time. Recorded once per document. |
| updated | Document HTML/PDF was replaced via PUT. Logged in history before the status resets. |
| awaiting_signatures | signable — waiting for all signers to submit. |
| all_signed | signable — all signers have submitted. Signatures being embedded into PDF. |
| awaiting_payment | payable or signable + payment — Stripe payment form active, waiting for payment. |
| paid | payable or signable + payment — payment received, PAID stamp being applied. |
| complete | All actions finished. Final PDF available for download. |
| draft | Document created with draft: true — PDF generated and stored but no emails sent and no Stripe payment intent created yet. Call POST /api/documents/:id/send to activate. |
| error | PDF generation failed. |
| deleted | Document removed by owner. Recipients see a "no longer available" page. |
All limits are enforced per fixed hourly or per-5-minute window. Responses include an HTTP 429 status when exceeded. Limits reset automatically at the start of each window.
| Endpoint | Key | Limit |
|---|---|---|
| POST /api/documents | Authenticated user (API key owner) | 100 / hour |
| POST /api/documents | IP address (shared / global key) | 20 / hour |
| POST /api/documents/:id/sign | IP address | 10 / 5 min |
| POST /api/auth/login | IP address | 10 / hour |
Every document has a type that determines its behavior, required parameters, and status lifecycle.
Use this section to quickly identify which fields apply to each type.
| Required | type, input source* |
| Optional | data, layout, metadata, draft, notify_emails |
| Status flow | created → hosted |
| Required | type, input source*, signers |
| Optional | data, layout, metadata, draft, notify_emails |
| Status flow | created → awaiting_signatures → all_signed → complete |
| Required | type, input source*, payer, amount |
| Optional | currency, data, layout, metadata, draft, notify_emails |
| Status flow | created → awaiting_payment → paid → complete |
| Required | type (signable), input source*, signers, payment |
| Optional | data, layout, metadata, draft, notify_emails |
| Status flow | created → awaiting_signatures → all_signed → awaiting_payment → paid → complete |
html, pdf_url, pdf_base64, or template_id
The following table shows which type-specific fields belong to which document type.
Using a field on the wrong type returns 400 Bad Request.
| Field | readable | signable | payable |
|---|---|---|---|
| signers | — | required | — |
| payer | — | — | required |
| amount | — | — | required |
| currency | — | — | optional |
| payment | — | optional | — |
Creates a document from HTML, a PDF upload, or a saved template.
Depending on type, signer invitations or a payment link are triggered automatically.
* Provide exactly one input source: html, pdf_url, pdf_base64, or template_id.
| Field | Type | Description |
|---|---|---|
| html | string | Full HTML document to convert to PDF. Supports Handlebars templating when paired with data. Max 500 KB. |
| pdf_url | string | Publicly accessible URL to an existing PDF file. Stored as-is — no HTML rendering. Max 10 MB. |
| pdf_base64 | string | Base64-encoded PDF content. Do not include the data: prefix. Max 10 MB. |
| template_id | string | ID of a saved template. Pair with data to fill {{variables}}. See Templates section. |
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | required | "readable", "signable", or "payable" |
| name | string | optional | Human-readable label for the document. Shown in the dashboard list and used as the PDF download filename (e.g. "Q3 2026 Invoice — Acme Corp" → Q3 2026 Invoice — Acme Corp.pdf). Max 200 characters; control chars forbidden. Pass an empty string or null on update to clear. |
| data | object | optional | JSON data for Handlebars template rendering. Supports {{variable}}, {{#each}}, {{#if}}. |
| layout | object | optional | PDF page layout: size, orientation, margins. See Layout section below. Defaults to Letter portrait. |
| metadata | array | optional | Up to 50 { "key": "...", "value": "..." } pairs. Keys ≤ 128 chars, values ≤ 1024 chars. Echoed back on GET /api/documents/:id and on every webhook payload. Convenience: a flat object ({ "key1": "v1", "key2": "v2" }) is also accepted and normalised to the array shape on output. |
| notify_emails | string[] | optional | Additional email addresses to notify on completion. |
| message | string | optional | Custom message included in the signer invite email. Appears as a bordered card between the header and body. Max 2,000 characters. |
| sandbox | boolean | optional | When true, no real emails are sent, no Stripe charges are created, and the document does not count against your monthly quota. Unlimited sandbox documents on all plans. Sandbox documents are automatically deleted after 24 hours. Defaults to false. |
| draft | boolean | optional | When true, PDF is generated but no emails are sent and no Stripe payment intent is created. Call POST /api/documents/:id/send when ready. Defaults to false. |
| Field | Type | Required | Description |
|---|---|---|---|
| signers | array | required |
Array of signer objects. At least one required, max 6.
email — required — signer's email addressname — optional — signer's display nameorder — optional — integer signing round (e.g. 1, 2).
When provided, signers with the lowest order receive their invite first.
Signers at the next round are only notified after everyone in the current round has signed.
Signers sharing the same order value receive their invite simultaneously.
Omit order on all signers to use the default parallel behavior (everyone notified at once).
|
| presign | object | optional |
Pre-populate slot 0 in the signature grid with the sender's own signature at document creation time — no signing link or email required.
Consent, IP address, and user agent are captured automatically from the API request and included in the full audit trail.
presign.signature_data_url — required — PNG/JPEG data URL (data:image/png;base64,…) or an HTTPS image URL (fetched server-side at creation time)presign.name — optional — display name shown in the signature footnotepresign.email — optional — email for audit trail (defaults to authenticated user's email)presign.printed_name — optional — typed/printed namepresign.timestamp — optional — ISO datetime of signing (defaults to request time)
|
| payment | object | optional |
Enables the sign-then-pay flow. Signatures are collected first; a Stripe PaymentIntent is created automatically after all parties sign.
payment.payer.email — required — payer's email (receives invoice after signing completes)payment.payer.name — optional — payer's display namepayment.amount — required — amount in smallest currency unit (e.g. 250000 = $2,500.00)payment.currency — optional — ISO 4217 code, defaults to "usd"
The payer can be one of the signers or a separate party. The final PDF contains both embedded signatures and a PAID watermark. |
| Field | Type | Required | Description |
|---|---|---|---|
| payer | object | required | { email: string, name?: string } — who receives the payment request. |
| amount | number | required | Amount in smallest currency unit (e.g. 4500 = $45.00 for USD; 4500 = ¥4,500 for JPY). Alternatively, embed data-total="<amount>" on the HTML <body> tag. |
| currency | string | optional | ISO 4217 code (e.g. "usd", "eur", "gbp", "jpy"). Defaults to "usd". Zero-decimal currencies pass the full unit amount. |
| destination_account | string | optional · Unlimited | Stripe Connect acct_… ID. When set, the payment is routed via transfer_data[destination] to that connected account. Non-Unlimited plans receive 403 TIER_REQUIRED. |
| platform_fee_percent | number | optional | 0–100. The cut your platform keeps. Applied as application_fee_amount on the PaymentIntent. Requires destination_account. If omitted, falls back to the org's default fee % set under Settings → Stripe → Marketplace payouts. |
Note: Top-level payer, amount, currency, destination_account, and platform_fee_percent are for type: "payable" only.
For signable documents with payment, use the nested payment object instead.
| Field | Type | Default | Description |
|---|---|---|---|
| size | string | "Letter" |
"A0" – "A6", "Letter", "Legal", "Tabloid", "Ledger" |
| orientation | string | "portrait" |
"portrait" or "landscape" |
| margin | string | object | "10px" |
A single CSS dimension applied to all sides (e.g. "20mm", "1in", "72pt"), or an object { top, right, bottom, left } for per-side control. Accepted units: px, mm, cm, in, pt. |
// A4 landscape with generous margins { "type": "readable", "html": "...", "layout": { "size": "A4", "orientation": "landscape", "margin": "20mm" } } // Per-side margin control "margin": { "top": "25mm", "right": "15mm", "bottom": "25mm", "left": "15mm" }
| Limit | Value |
|---|---|
| html size | Max 500 KB |
| signers | Max 6 per document |
| PDF pages | Max 100 pages for signing/stamping |
| PDF file size | Max 20 MB for signing/stamping |
| Rate limit | 100 requests/hour per authenticated user · 20/hour per IP for shared keys |
// html field (Handlebars template) "<h1>Invoice for {{client.name}}</h1> <table> {{#each lineItems}} <tr><td>{{description}}</td><td>${{amount}}</td></tr> {{/each}} </table> {{#if isPaid}}<p>PAID</p>{{/if}}" // data field { "client": { "name": "Jane Smith" }, "lineItems": [ { "description": "Consulting", "amount": "3,000.00" }, { "description": "Expenses", "amount": "1,500.00" } ], "isPaid": false }
"id": "4b860f6c...", // Document ID "url": "https://zipzign.com/doc/4b860f6c...", "status": "hosted" // | "awaiting_signatures" | "awaiting_payment" | "draft"
type, invalid layout values, oversized html (>500 KB), or Handlebars template syntax error.curl -X POST https://zipzign.com/api/documents \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "type": "readable", "html": "<!DOCTYPE html><html><body><h1>Hello</h1></body></html>" }'
curl -X POST https://zipzign.com/api/documents \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "type": "signable", "html": "<!DOCTYPE html>...</html>", "signers": [ { "email": "alice@example.com", "name": "Alice" }, { "email": "bob@example.com" } ], "metadata": [ { "key": "contract_id", "value": "contract_abc123" }, { "key": "customer_id", "value": "cust_9876" } ] }'
curl -X POST https://zipzign.com/api/documents \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "type": "payable", "html": "<!DOCTYPE html>...</html>", "amount": 450000, "currency": "usd", "payer": { "email": "client@example.com", "name": "Jane Smith" } }'
curl -X POST https://zipzign.com/api/documents \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "type": "payable", "html": "<!DOCTYPE html>...</html>", "amount": 450000, "currency": "eur", "payer": { "email": "client@example.com" } }'
// Collect signatures first, then collect payment curl -X POST https://zipzign.com/api/documents \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "type": "signable", "html": "<!DOCTYPE html>...</html>", "signers": [ { "email": "alice@co.com", "name": "Alice" }, { "email": "bob@client.com", "name": "Bob" } ], "payment": { "payer": { "email": "bob@client.com", "name": "Bob" }, "amount": 250000, "currency": "usd" } }' // Status flow: // 1. awaiting_signatures — signers receive invite emails // 2. all_signed — signatures embedded into PDF // 3. awaiting_payment — payer receives invoice email, payment form shown // 4. paid → complete — PAID stamp applied, confirmation emails sent
Returns a paginated list of documents belonging to the API key owner.
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number |
| per_page | integer | 20 | Results per page (max 100) |
| status | string | — | Filter: hosted, awaiting_signatures, awaiting_payment, all_signed, paid |
| type | string | — | Filter: readable, signable, payable |
| created_from | string | — | ISO date start (e.g. 2026-01-01) |
| created_to | string | — | ISO date end |
| sort | string | desc | asc or desc by created_at |
| sandbox | string | false | Set true to list sandbox documents only. Note: sandbox documents expire after 24 hours. |
curl "https://zipzign.com/api/documents?page=1&type=signable" \ -H "Authorization: Bearer <KEY>"
{
"count": 42,
"page": 1,
"per_page": 20,
"results": [
{
"id": "abc123...",
"type": "signable",
"status": "awaiting_signatures",
"amount": null,
"currency": "usd",
"sandbox": false,
"url": "https://zipzign.com/doc/abc123...",
"created_at": "2026-04-09T12:00:00.000Z",
"updated_at": "2026-04-09T12:00:00.000Z"
}
]
}
Returns full document metadata, a short-lived PDF URL, and Stripe payment fields (for payable documents awaiting payment).
| Parameter | Type | Description |
|---|---|---|
| id | string | Document ID returned from POST /api/documents. |
{
"id": "4b860f6c...",
"type": "signable",
"status": "awaiting_signatures",
"pdf_url": "https://zipzign.com/pdf/4b860f6c...", // Stable, embeddable — no auth required
"amount": null, // Smallest currency unit, payable only
"currency": "usd", // ISO 4217 — always present
"layout": { // Only if layout was specified at creation
"size": "A4",
"orientation": "portrait",
"margin": "20mm"
},
"metadata": [ // Only if metadata was set at creation
{ "key": "contract_id", "value": "contract_abc123" },
{ "key": "customer_id", "value": "cust_9876" }
],
"signers": [ // Signable only
{
"id": "...",
"email": "alice@example.com",
"name": "Alice",
"signed_at": null // ISO timestamp when signed
}
],
"stripe_publishable_key": "pk_live_...", // Payable awaiting_payment only
"stripe_client_secret": "pi_...secret_..." // Payable awaiting_payment only
}
Replaces a document's content (HTML, template, or PDF) and resets it to its active status
(hosted / awaiting_signatures / awaiting_payment).
The document type, signers, and payer are unchanged — only the content is replaced.
Rename without re-rendering: Pass only name (no content fields) to update
the document's label without touching the PDF, status, or daily edit budget. Pass null
or an empty string to clear the name. Allowed on any document that isn't deleted.
Not available if the document has already been paid, signed, or completed.
Async for html/template inputs: Returns immediately with status: "created".
Poll doc_url until the status transitions to its active state. For pdf_url
and pdf_base64 inputs the response reflects the final status directly.
Idempotent / free re-saves: Calls with unchanged content (same HTML + same layout)
short-circuit without re-rendering the PDF — the response includes "cached": true and
returns instantly. These calls do not count against the daily edit budget.
Per-tier daily edit cap: Each render-triggering edit (html/template inputs only —
pdf_url and pdf_base64 are exempt) consumes one slot in your plan's
daily edit budget: Free 5/day · Starter 100/day · Pro 500/day · Unlimited none.
When exhausted, returns 429 with code: "EDIT_LIMIT" and a
retry_after_seconds hint.
| Field | Type | Required | Description |
|---|---|---|---|
| html | string | optional* | Raw HTML converted to PDF via Browserless. Mutually exclusive with template_id, pdf_url, and pdf_base64. |
| template_id | string | optional* | ID of a saved template. The template HTML is rendered with data and converted to PDF. |
| pdf_url | string | optional* | URL of a PDF to use as the new document. Accepts https://zipzign.com/pdf/… or any public URL. |
| pdf_base64 | string | optional* | Base64-encoded PDF bytes to use as the new document. |
| data | object / string | optional | Handlebars template variables applied to html or template_id. Also accepts a JSON string (Make-compatible). |
| layout | object | optional | PDF page layout for html/template_id inputs. See Layout Object above. |
| name | string | null | optional | New document name (max 200 chars). Pass alone to rename without re-rendering — content fields become optional in that case. Pass null or "" to clear. |
| notify | boolean | optional | If true, re-sends the invite or invoice email to all recipients after the PDF is ready. Defaults to false. |
| message | string | optional | Custom message included in the re-sent signer invite email when notify: true. Appears as a bordered card in the email body. Only applies to signable documents. |
* Exactly one of html, template_id, pdf_url, or pdf_base64 is required unless the body is a name-only update.
{
"id": "4b860f6c...",
"type": "signable",
"status": "created",
"pdf_url": "https://zipzign.com/pdf/4b860f6c...",
"doc_url": "https://zipzign.com/doc/4b860f6c..."
}
doc_url for final status (html/template inputs).data, invalid layout, or template render error.code: "EDIT_LIMIT"). See retry_after_seconds.
Appends new pages onto the end of an existing document's PDF without changing its type, status, metadata, or any other properties.
Supports the same five input methods as POST /api/documents.
Available for documents with status hosted or draft. Signable and payable documents are immutable once sent to recipients — this is standard practice to preserve legal and financial integrity. Create a document with draft: true to append pages before sending.
Per-tier daily edit cap: PATCH triggers a synchronous Browserless render, so each
call consumes one slot in your plan's daily edit budget (Free 5/day · Starter 100/day ·
Pro 500/day · Unlimited none). Calls that would produce identical combined HTML are
free and return "cached": true. Exceeding the cap returns 429 with
code: "EDIT_LIMIT".
| Field | Type | Required | Description |
|---|---|---|---|
| html | string | optional | HTML rendered to PDF pages and appended. Max 500 KB. |
| data | object | optional | Handlebars template data applied to html or a template before rendering. |
| layout | object | optional | PDF layout options (size, orientation, margin) for HTML-based inputs. See Layout Object above. |
| pdf_url | string | optional | URL of a PDF whose pages are appended. Must resolve to a valid PDF (validated via %PDF- magic bytes). Max 10 MB. |
| pdf_base64 | string | optional | Base64-encoded PDF (data URI prefix accepted). Must be a valid PDF. Max 10 MB decoded. |
| template_id | string | optional | ID of a saved template whose rendered HTML is appended as new pages. |
{
"id": "4b860f6c...",
"type": "readable",
"status": "hosted",
"pdf_url": "https://zipzign.com/pdf/4b860f6c...",
"amount": null
}
layout, invalid data, or html too large.template_id not found.hosted or draft (signable/payable docs are immutable once sent).code: "EDIT_LIMIT"). See retry_after_seconds.
Activates a document previously created with draft: true. This is a one-way transition —
once sent, the document behaves exactly as if it had been created without draft:
• readable — status transitions to hosted; the document becomes publicly accessible.
• signable — status transitions to awaiting_signatures; invite emails are sent to all signers.
• signable + payment — same as signable; payment phase triggers automatically after all signatures are collected.
• payable — a Stripe payment intent is created, status transitions to awaiting_payment, and a payment-request email is sent to the payer.
A document.status_changed webhook event fires on success. No request body is required.
{
"id": "4b860f6c...",
"type": "signable",
"status": "awaiting_signatures",
"pdf_url": "https://zipzign.com/pdf/4b860f6c...",
"amount": null
}
draft status. Only draft documents can be sent.curl -X POST https://zipzign.com/api/documents/4b860f6c.../send \ -H "Authorization: Bearer <KEY>"
Adds a signing or payment phase to an existing document. The document's current PDF is preserved — no new content upload required.
This enables multi-step workflows where you create a readable document first, then later add signatures or payment collection.
The request body must include either signers (to collect signatures) or payer + amount (to collect payment) — not both.
For combined sign-then-pay in a single step, use the payment object on POST /api/documents instead.
Document must be in hosted or complete status. Documents that are mid-signing, mid-payment, deleted, or in error state return 409.
| Field | Type | Required | Description |
|---|---|---|---|
| signers | array | required | Array of { email: string, name?: string }. Max 20. |
| notify_emails | string[] | optional | Additional emails to notify on completion. |
| Field | Type | Required | Description |
|---|---|---|---|
| payer | object | required | { email: string, name?: string } |
| amount | number | required | Amount in smallest currency unit. |
| currency | string | optional | ISO 4217 code. Defaults to "usd". |
| notify_emails | string[] | optional | Additional emails to notify on payment. |
// readable (hosted) → signable (awaiting_signatures) curl -X POST https://zipzign.com/api/documents/4b860f6c... \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "signers": [ { "email": "alice@co.com", "name": "Alice" }, { "email": "bob@co.com", "name": "Bob" } ] }'
// signable (complete) → payable (awaiting_payment) curl -X POST https://zipzign.com/api/documents/4b860f6c... \ -H "Authorization: Bearer <KEY>" \ -H "Content-Type: application/json" \ -d '{ "payer": { "email": "billing@client.com", "name": "Jane" }, "amount": 250000, "currency": "usd" }'
hosted or complete status.Marks a document as deleted. Only documents that have not been paid or signed can be deleted. Deleted documents return a "no longer available" page when visited by end users.
{ "success": true }
Create and send one independent signable document per recipient in a single API call.
Each recipient receives their own copy with a unique signing link — recipients cannot see each other's signatures.
Ideal for mass NDAs, consent forms, or agreements sent to a list of contacts.
Plan requirement: Bulk send is exclusive to the Unlimited plan.
Lower tiers receive a 403 response with required_tier: "unlimited".
| Field | Type | Required | Description |
|---|---|---|---|
| recipients | array | required | Array of { email: string, name?: string }. Max 20 per call. |
| template_id | string | optional* | Template ID to render for each document. Mutually exclusive with html. |
| html | string | optional* | Raw HTML source. Either template_id or html is required. |
| data | object | optional | Template variables applied to every document. |
| message | string | optional | Custom message included in every signer invite email. |
| notify_emails | array | optional | Email addresses to notify when each document is signed. |
| sandbox | boolean | optional | Create sandbox documents (no real emails, auto-deleted after 24 h). |
{
"created": 3,
"documents": [
{ "document_id": "abc123", "email": "alice@co.com", "doc_url": "https://zipzign.com/doc/abc123" },
{ "document_id": "def456", "email": "bob@co.com", "doc_url": "https://zipzign.com/doc/def456" }
]
}
Send a reminder email to all pending (unsigned) signers for a signable document. For documents using sequential signing rounds, only signers in the active round are reminded. Rate-limited: each signer can only be reminded once per hour.
{
"ok": true,
"reminded": [
{ "email": "alice@co.com", "name": "Alice" }
]
}
Settle a payable document out-of-band — useful when the payer pays by wire, check, or cash.
ZipZign flips the document to paid, cancels any open Stripe PaymentIntent so the hosted
checkout link stops working, stamps the PAID watermark, sends the payment-confirmation email,
and fires document.paid + document.status_changed webhooks just as if Stripe had confirmed payment.
One-way: there is no "un-mark paid" endpoint. Use carefully.
| Field | Type | Required | Description |
|---|---|---|---|
| method | string | optional | Free-form note describing how payment was received (e.g. "wire", "check", "cash"). Defaults to "manual". Max 64 chars. Echoed in webhook payloads as paymentMethod so downstream systems can distinguish manual vs. Stripe-confirmed payments. |
{
"ok": true,
"status": "paid",
"method": "wire",
"stripe_payment_intent_canceled": true
}
Submit a signer's signature. The token comes from the signing link emailed to each signer. When all parties have signed, the final PDF is assembled and a completion email sent.
GET /api/documents/:id/verify.
Signatures meet the Simple Electronic Signature (SES) standard under the ESIGN Act and UETA —
legally sufficient for the vast majority of commercial agreements.
EU customers: SES is valid under eIDAS for many commercial transactions;
Advanced (AES) and Qualified (QES) signature tiers are on the roadmap for EU market expansion.
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Signer-specific token from the invitation email URL. |
| Field | Type | Required | Description |
|---|---|---|---|
| signature_data_url | string | required | Base64 PNG data URL: data:image/png;base64,... |
| consent_given | boolean | required | Must be true. Confirms the signer has given affirmative consent to sign electronically (ESIGN Act / UETA requirement). |
| printed_name | string | optional | Signer's full name as printed text. Appears in the signature footnote and Certificate of Completion. |
| Limit | Value |
|---|---|
| signature_data_url | Max 200 KB |
| Rate limit | 10 attempts / 5 minutes per IP |
{
"success": true,
"all_signed": false // true when every signer has submitted
}
signature_data_url format.signature_data_url exceeds 200 KB.Public endpoint (no authentication required). Recomputes the SHA-256 hash of the stored signed PDF and compares it to the hash recorded at signing time. Use this to verify a document has not been tampered with after signing.
{
"valid": true,
"document_id": "abc123",
"stored_hash": "e3b0c44298fc1c14...",
"computed_hash": "e3b0c44298fc1c14..."
}
Returns the full signing audit trail for a document: signer details (consent, IP, user agent) and chronological signing events. Requires API key authentication.
{
"document_id": "abc123",
"pdf_hash": "e3b0c44...",
"signers": [{
"email": "alice@co.com",
"name": "Alice",
"signed_at": "2026-04-14T...",
"consent_given": true,
"consent_timestamp": "2026-04-14T...",
"consent_version": "1.0",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0..."
}],
"events": [{
"event_type": "consent_given",
"signer_id": "sig_abc...",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0...",
"metadata": { "consent_version": "1.0" },
"created_at": "2026-04-14T..."
}]
}
Returns the document's PDF directly — no redirect. Serves the final PDF (with embedded signatures or PAID stamp) when available, otherwise the original.
The URL is stable, permanent, and publicly accessible for all document types. Security is provided by the unguessable 128-bit document ID.
PDFs are served with X-Frame-Options: ALLOWALL so they can be embedded in <iframe> or <embed> tags from any origin.
| Header | Value |
|---|---|
| Content-Type | application/pdf |
| Content-Disposition | inline; filename="document-<id>.pdf" |
| X-Frame-Options | ALLOWALL — safe to embed in iframes |
| Cache-Control | public, max-age=3600 |
<iframe src="https://zipzign.com/pdf/4b860f6c..." width="100%" height="800" style="border:none" />
Save a reusable HTML template with {{variable}} placeholders.
Variables are auto-detected from the HTML. Use template_id in POST /api/documents to create documents from this template.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Template name (max 255 chars) |
| html | string | required | HTML with {{variable}} placeholders (max 500 KB). Supports full Handlebars syntax — {{#each}}, {{#if}}, helpers, etc. |
curl -X POST https://zipzign.com/api/templates \ -H "Cookie: session=<TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "name": "Invoice Template", "html": "<h1>Invoice for {{client_name}}</h1><p>Total: {{total}}</p>" }'
201){
"id": "tpl_abc123...",
"name": "Invoice Template",
"variables": ["client_name", "total"], // auto-detected from {{...}} placeholders
"created_at": "2026-04-09T12:00:00.000Z"
}
// Document type is set at creation time, not on the template { "template_id": "tpl_abc123...", "type": "payable", "amount": 450000, "currency": "usd", "data": { "client_name": "Acme Corp", "total": "$4,500" }, "payer": { "email": "billing@acme.com" } }
Returns all templates belonging to the authenticated user.
Returns the full template including HTML source and detected variables.
Update the template's name and/or HTML. Variables are re-detected automatically when HTML changes. Existing documents created from this template are not affected.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | New template name |
| html | string | optional | New HTML source with {{variable}} placeholders |
Permanently delete a template. Existing documents created from this template are not affected.
{ "ok": true }Webhooks
Register HTTP endpoints to receive real-time event notifications whenever a document changes state. Each endpoint has its own independent signing secret. You can register up to 10 endpoints per account — useful for sending events to multiple services (e.g. your backend, Zapier, Make).
When an event fires, ZipZign sends an HTTP POST to each of your enabled endpoints. The request body is JSON and always includes the event type, a unique event ID, an ISO timestamp, and the document ID.
{
"event": "document.signed",
"id": "evt_3f8a1c...",
"timestamp": "2026-04-11T14:32:00.000Z",
"document_id": "a1b2c3d4...",
"document": {
"id": "a1b2c3d4...",
"type": "signable",
"status": "complete",
"doc_url": "https://zipzign.com/doc/a1b2c3d4...",
"pdf_url": "https://zipzign.com/api/pdf/a1b2c3d4...",
"amount": null,
"currency": "usd",
"payer_email": null,
"payer_name": null,
"sandbox": false,
"created_at": "2026-04-11T14:00:00.000Z",
"updated_at": "2026-04-11T14:32:00.000Z",
"metadata": [{ "key": "orderId", "value": "ORD-123" }],
"signers": [
{ "email": "alice@co.com", "name": "Alice", "signed_at": "2026-04-11T14:32:00.000Z" }
]
}
}
| Field | Type | Description |
|---|---|---|
| doc_url | string | Public URL to view/sign the document. |
| pdf_url | string | null | Direct URL to download the PDF. null if no PDF exists yet. |
| amount | number | null | Payment amount in smallest currency unit (cents). null for non-payable documents. |
| currency | string | Three-letter ISO currency code. |
| payer_email | string | null | Payer's email address, if applicable. |
| payer_name | string | null | Payer's name, if provided. |
| sandbox | boolean | Whether this is a sandbox/test document. |
| created_at | string | ISO 8601 creation timestamp. |
| updated_at | string | ISO 8601 last-updated timestamp. |
| metadata | array | Custom key/value pairs, if set. Omitted when empty. |
| signers | array | Signer list with email, name, and signed_at. Only present for signable documents. |
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-ZipZign-Signature | HMAC-SHA256 signature — see Signature Verification below |
| User-Agent | ZipZign-Webhooks/1.0 |
Your endpoint must respond with any 2xx status within 10 seconds.
Delivery failures are not automatically retried — design your receiver to be idempotent and poll
GET /api/documents/:id/status if you need guaranteed delivery confirmation.
| Scenario | What happens |
|---|---|
| Endpoint responds 2xx | Delivery recorded as successful. No further action. |
| Endpoint responds 3xx | Treated as failure — ZipZign does not follow redirects. |
| Endpoint responds 4xx / 5xx | Delivery failed. Event is dropped — not retried. Check your endpoint logs. |
| No response within 10 s | Connection times out. Delivery failed, not retried. |
| Connection refused / DNS failure | Delivery failed immediately, not retried. Verify your URL is reachable from the public internet. |
💡 Best practice: Use the id field in every payload as an idempotency key. If your endpoint misses an event, recover by calling
GET /api/documents/:id/status or GET /api/documents/:id/audit.
Webhook delivery logs and automatic retries are on the roadmap.
| Event | Fired when |
|---|---|
document.created |
A new document has been created via the API. |
document.status_changed |
The document status transitions to any new value (hosted, awaiting_signatures, awaiting_payment, paid, complete, error). |
document.signed |
All required signers have signed — document is now complete. |
document.paid |
Payment has been confirmed via Stripe — document is now complete. |
webhook.test |
Fired manually from the dashboard or via the test endpoint. Use this to verify your integration. |
Every request includes an X-ZipZign-Signature header of the form
t=<unix_ms>,v1=<hex>. The signature is an HMAC-SHA256 over the
string <timestamp>.<raw_json_body> using your endpoint's signing secret.
This format is identical to Stripe's webhook signatures.
const crypto = require("crypto");
function verifyZipZignWebhook(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map(p => p.split("="))
);
const timestamp = parts["t"];
const expected = parts["v1"];
if (!timestamp || !expected) return false;
// Reject events older than 5 minutes
if (Math.abs(Date.now() - Number(timestamp)) > 300_000) return false;
const sig = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Register a new webhook endpoint. Returns the signing secret — store it securely. Up to 10 endpoints per account.
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | required | The HTTPS URL to receive events |
| label | string | optional | Friendly name, e.g. "Production" or "Zapier" |
| enabled | boolean | optional | Whether to deliver events. Defaults to true |
{
"id": "a1b2c3...",
"label": "Production",
"url": "https://your-app.com/webhooks/zipzign",
"secret": "3f8a1c...", // store this — shown only once
"enabled": true,
"created_at": "2026-04-11T14:00:00.000Z"
}
Returns all registered endpoints for the authenticated user, including their signing secrets.
Update the URL, label, or enabled state of an endpoint. The signing secret is preserved when the URL changes.
| Field | Type | Description |
|---|---|---|
| url | string | New endpoint URL |
| label | string | New friendly name |
| enabled | boolean | Enable or disable delivery |
Permanently removes the endpoint and its signing secret.
{ "ok": true }
Immediately fires a webhook.test event to the specified endpoint. Useful for verifying your integration is working correctly.
{ "ok": true, "url": "https://..." }Custom Email Domain
Pro and Unlimited organizations can configure a custom domain so that all outbound document emails
(signer invites, payment invoices, completion notifications) are sent from their own address —
e.g. noreply@yourcompany.com — instead of hello@zipzign.com.
Requires adding DNS records to verify ownership. Only one domain per organization.
Returns the current custom email domain for the organization, or null if none is configured.
| Field | Type | Description |
|---|---|---|
| id | string | Internal domain ID (from Resend) |
| domain | string | Verified domain (e.g. acme.com) |
| from_email | string | Full from address (e.g. noreply@acme.com) |
| from_name | string|null | Display name in the From header |
| status | string | pending | verified | failed |
| dns_records | array|null | DNS records to add. Each record has type, name, value, status. |
| created_at | string | ISO 8601 timestamp |
Auth: Session required. Tier: Any (returns null if unpaid).
Registers a domain with the email provider and returns DNS records to add. The domain remains pending until DNS records are added and verified.
| Parameter | Type | Required | Description |
|---|---|---|---|
| domain | string | Yes | Domain to verify, e.g. acme.com |
| from_email | string | Yes | Full from address — must end with @{domain} |
| from_name | string | No | Display name in the From header. Falls back to brand name. |
Auth: Session required. Tier: Pro or Unlimited only (returns 403 otherwise).
Errors: 403 if not Pro/Unlimited · 409 if domain already configured · 400 for invalid inputs · 502 if email provider rejects the domain.
{
"domain": "acme.com",
"from_email": "contracts@acme.com",
"from_name": "Acme Corp"
}
Asks the email provider to re-check DNS records. Call this after adding the required records to trigger verification. Returns the updated status and per-record statuses.
Auth: Session required. Errors: 404 if no domain configured.
Removes the domain from the email provider and clears the configuration. Emails immediately revert to being sent from hello@zipzign.com.
Auth: Session required. Errors: 404 if no domain configured.
Teams
ZipZign supports multi-user workspaces (organizations). Every user starts with a personal workspace. Owners and admins can invite teammates — once accepted, all members share the same documents, templates, API keys, branding, and subscription quota. Roles control what each member can do.
| Role | Description |
|---|---|
| owner | Full access. Can rename the workspace, manage billing, change any member's role, and remove other owners (as long as at least one owner remains). |
| admin | Can invite, remove, and manage member/viewer roles. Cannot remove owners or access billing. |
| member | Can create and send documents. Cannot manage other members. |
| viewer | Read-only access to documents. |
All data endpoints (documents, templates, webhooks, API keys, branding) scope their results to
a single workspace. Pass the workspace ID via the X-Org-Id request header to select
which workspace a request operates against.
If the header is omitted, the request falls back to the caller's personal workspace.
Session-authenticated requests that pass an X-Org-Id the user is not a member of
receive 403 Forbidden.
Authorization: Bearer ds_… X-Org-Id: org_abc123def456
API-key-authenticated requests inherit the workspace the key was created in — no header needed unless the key owner belongs to multiple workspaces and wants to override.
Requires session auth. Any member of the workspace may call this.
[
{
"id": "mem_abc123",
"user_id": "2d7c352e…",
"email": "alice@example.com",
"name": "Alice Smith",
"role": "member",
"joined_at": "2024-01-15T10:00:00.000Z",
"is_self": false
}
]Requires admin or owner role. Sends an invitation email. Invitations expire after 14 days. Only one pending invite per (org, email) is allowed at a time.
| Field | Type | Description |
|---|---|---|
| email required | string | Email address to invite. |
| role | string | Role to grant on acceptance: owner, admin, member (default), or viewer. Admins can only invite member or viewer. |
{
"id": "inv_abc123",
"email": "bob@example.com",
"role": "member",
"invited_at": "2024-01-15T10:00:00.000Z",
"expires_at": "2024-01-29 10:00:00",
"accept_url": "https://zipzign.com/invite/abc123…"
}Requires session auth. The logged-in user's email must match the invitation's target email. On success, creates a membership row and returns the new org_id and role.
{ "ok": true, "org_id": "org_abc123", "role": "member" }Returns 410 Gone if the invitation has been revoked, already accepted, or expired.
Requires owner role. Cannot demote the last owner.
| Field | Type | Description |
|---|---|---|
| role required | string | owner, admin, member, or viewer. |
Admin/owner required to remove others. Any member may remove themselves (leave) except:
the last owner of a workspace, and any member of their personal workspace.
Returns 409 when the last-owner constraint would be violated.
Creates a non-personal workspace and makes the caller its owner. The new workspace starts on the free plan with its own independent quota.
| Field | Type | Description |
|---|---|---|
| name required | string | Display name for the workspace. Max 255 characters. |
{ "id": "org_abc123", "name": "Acme Corp", "role": "owner", "is_personal": 0 }Integrations
ZipZign works with AI coding tools via MCP, and with no-code platforms via Zapier and Make.com.
The ZipZign MCP server exposes the full document API as named tools that any MCP-compatible AI coding tool can call directly —
Claude Code, Cursor, Continue, Cline, and others. Install it once; every session in that tool has a working ZipZign toolset.
No curl, no SDK install, no schema lookup required.
claude mcp add zipzign https://zipzign.com/mcp \ --header "Authorization: Bearer YOUR_API_KEY"
https://zipzign.com/mcp
Transport: Streamable HTTP (stateless, no session management). Auth: existing ZipZign API key passed via Authorization: Bearer ds_… header.
| Tool | Description |
|---|---|
| me | Verify the API key; return email and user ID. |
| list_documents | List documents with optional type/status filters and pagination. |
| get_document | Fetch full document state including signers, URLs, and metadata. |
| create_document | Create a readable, signable, or payable document. |
| update_document | Replace document content (HTML / template / PDF URL). Includes dedup + daily edit cap. |
| append_document | Append HTML pages to an existing draft document. |
| send_document | Send a draft — triggers signing/payment emails. |
| delete_document | Soft-delete a document. |
| replace_signer | Swap an unsigned signer atomically; sends a fresh invite to the new email. |
| get_audit_trail | Retrieve the full audit log for compliance and debugging. |
- "What's my ZipZign workspace called?"
- "Create a sandbox signable NDA with this HTML for alice@example.com"
- "Show me my 5 most recent documents"
- "Replace alice@x.com with carol@x.com on doc abc123"
- "What does the audit trail say for that document?"
The AI will only set sandbox: true on create_document if you ask for it explicitly. Vague phrases like "create a test document" often produce a live document — which counts against your monthly quota and sends real emails to any signers.
Be explicit: "Create a sandbox readable document with this HTML…" or "…in sandbox mode for alice@example.com". Sandbox documents are free, suppress emails and Stripe charges, and auto-delete after 24 hours.
All tools inherit the same rate limits, quota checks, and RBAC as the REST API. Available on Pro and Unlimited plans. Free and Starter API keys receive a 403 response from the MCP endpoint.
The official ZipZign Zapier app lets you connect ZipZign to 8,000+ other apps without writing code. Use triggers like New Signed Document or New Paid Document to kick off downstream automations, or use actions like Create Document to generate signable documents when something happens elsewhere.
- New Signed Document
- New Paid Document
- Document Status Changed
- New Document
- Create Document (readable, signable, or payable)
- Update Document Content
- Send Draft Document
- Delete Document
- Find Document by ID / List Documents
Authentication uses your ZipZign API key. Available on every plan, including Free.
Make.com (formerly Integromat) works with ZipZign via generic HTTP modules. Use the HTTP Make a request module to POST to /api/documents with your API key, and the Webhooks module to receive real-time events. Available on every plan, including Free.
- Add an HTTP module. Set URL to
https://zipzign.com/api/documents, methodPOST. - Under Headers, add
Authorization: Bearer ds_…. - Set body type to JSON and map fields from your upstream trigger.
- For inbound events, create a Make webhook and register it at
/api/webhooks.
GDPR & Privacy
Endpoints for data export, subject erasure requests, document retention policies, and account deletion.
All endpoints require session authentication. Write operations require admin or owner role.
Download a full JSON archive of the current workspace. Includes documents (with signers and status history), templates, API key metadata (prefixes only — not hashes), webhook configurations, branding, and subscription tier. Scoped to the calling user's current workspace.
Auth: Session cookie (any role). Returns application/json as an attachment.
{
"exported_at": "2024-01-15T10:00:00.000Z",
"account": { "email": "you@example.com", "first_name": "Alice", "created_at": "..." },
"organization": { "id": "org_abc", "name": "My Workspace", "created_at": "..." },
"documents": [ { "id": "doc_xyz", "type": "signable", "status": "complete", "signers": [...], "history": [...] } ],
"templates": [ { "id": "tpl_abc", "name": "NDA", "created_at": "..." } ],
"api_keys": [ { "prefix": "ds_1a2b", "sandbox": false, "last_used_at": "..." } ],
"webhooks": [ { "url": "https://...", "events": ["document.signed"], "enabled": true } ],
"branding": { "company_name": "Acme", "primary_color": "#2563eb" },
"subscription": { "tier": "pro", "current_period_end": "2024-02-15" }
}
Anonymizes all records tied to a specific email address within the caller's workspace. Replaces the
signer's name and email (and payer email on documents) with [erased]. Use to fulfill
GDPR Article 17 "Right to Erasure" requests. This action is irreversible.
Auth: Session cookie, admin role or higher.
| Field | Type | Description |
|---|---|---|
email | string | Required. Email address of the data subject. |
reason | string | Optional. Free-text reason for the erasure request (kept in your audit trail). |
POST /api/user/erasure
{ "email": "alice@example.com", "reason": "GDPR request received 2024-01-15" }
{ "ok": true, "erased_email": "alice@example.com", "signer_records_anonymized": 3, "payer_records_anonymized": 1, "total_anonymized": 4 }
Returns the workspace's current document retention policy.
{ "retention_days": 365, "description": "Documents older than 365 days are automatically soft-deleted." }
retention_days: null means documents are retained indefinitely (default).
Sets or clears the workspace's document retention policy. When set, documents older than
retention_days that are not in a terminal status (all_signed, complete, paid)
will be soft-deleted nightly. Minimum value is 30 days.
Auth: Session cookie, owner role.
| Field | Type | Description |
|---|---|---|
retention_days | integer | null | Days to retain documents. Must be ≥ 30. Pass null to disable. |
PUT /api/user/account/retention
{ "retention_days": 365 }
PUT /api/user/account/retention
{ "retention_days": null }
Permanently and irreversibly deletes the caller's personal workspace and all associated data: documents, signers, templates, API keys, webhooks, branding, and billing records. The user record itself is also deleted and all sessions are invalidated. The caller is also removed from any shared workspaces they belong to (those workspaces continue for remaining members).
Auth: Session cookie, owner role on the personal workspace.
Must be called from the personal workspace context (not a shared org).
Prerequisite: Any active paid subscription must be cancelled first.
| Field | Type | Description |
|---|---|---|
confirm | string | Required. Must be the exact string "DELETE MY ACCOUNT". |
DELETE /api/user/account
{ "confirm": "DELETE MY ACCOUNT" }
204 No Content