ZipZign API

Generate hosted PDFs, collect e-signatures, and accept payments — all from a single POST request.

Base URL https://zipzign.com
⚡ Quickstart

From zero to a signed document in under 5 minutes. You'll need an API key from your dashboard.

1 Create a document — POST /api/documents
# 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.

2 Collect a signature — add signers to send them an email invite
# 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.

3 Collect payment — requires your Stripe keys in the dashboard
# 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.

🧪 Test without side effects: Add "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.
Full Create Document reference → Set up webhooks →
Authentication

Protected endpoints require an API key passed as a Bearer token in the Authorization header.

# All protected requests
Authorization: Bearer <YOUR_API_KEY>
Requests missing a valid key receive 401 Unauthorized. Contact the API owner to obtain a key.
Zero-Downtime Key Rotation

ZipZign supports multiple active API keys simultaneously. To rotate a key without interruption:

  1. Generate a new API key in the dashboard.
  2. Update your integration to use the new key and verify it works.
  3. Delete the old key from the dashboard.

Both keys are valid simultaneously during the transition window — no downtime required.

Document Statuses

A document moves through the following statuses over its lifecycle:

StatusDescription
createdInitial state, PDF being generated.
hostedReadable document is live and accessible.
viewedDocument was opened by a recipient for the first time. Recorded once per document.
updatedDocument HTML/PDF was replaced via PUT. Logged in history before the status resets.
awaiting_signaturessignable — waiting for all signers to submit.
all_signedsignable — all signers have submitted. Signatures being embedded into PDF.
awaiting_paymentpayable or signable + payment — Stripe payment form active, waiting for payment.
paidpayable or signable + payment — payment received, PAID stamp being applied.
completeAll actions finished. Final PDF available for download.
draftDocument 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.
errorPDF generation failed.
deletedDocument removed by owner. Recipients see a "no longer available" page.
Rate Limits

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.

EndpointKeyLimit
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
Type Reference

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.

readable View-only
Requiredtype, input source*
Optionaldata, layout, metadata, draft, notify_emails
Status flowcreatedhosted
signable E-signatures
Requiredtype, input source*, signers
Optionaldata, layout, metadata, draft, notify_emails
Status flowcreatedawaiting_signaturesall_signedcomplete
payable Invoice + payment
Requiredtype, input source*, payer, amount
Optionalcurrency, data, layout, metadata, draft, notify_emails
Status flowcreatedawaiting_paymentpaidcomplete
signable + payment NEW
Requiredtype (signable), input source*, signers, payment
Optionaldata, layout, metadata, draft, notify_emails
Status flowcreatedawaiting_signaturesall_signedawaiting_paymentpaidcomplete
* Input source — provide exactly one: html, pdf_url, pdf_base64, or template_id
#Parameter Compatibility

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
Endpoints
POST /api/documents Create a document
🔑 Requires API key

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.

#Input Source (provide exactly one)
FieldTypeDescription
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.
#Shared Parameters (all types)
FieldTypeRequiredDescription
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.
#Signable Parameters
FieldTypeRequiredDescription
signers array required Array of signer objects. At least one required, max 6.

emailrequired — signer's email address
nameoptional — signer's display name
orderoptional — 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_urlrequired — PNG/JPEG data URL (data:image/png;base64,…) or an HTTPS image URL (fetched server-side at creation time)
presign.nameoptional — display name shown in the signature footnote
presign.emailoptional — email for audit trail (defaults to authenticated user's email)
presign.printed_nameoptional — typed/printed name
presign.timestampoptional — 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.emailrequired — payer's email (receives invoice after signing completes)
payment.payer.nameoptional — payer's display name
payment.amountrequired — amount in smallest currency unit (e.g. 250000 = $2,500.00)
payment.currencyoptional — 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.
#Payable Parameters
FieldTypeRequiredDescription
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.

#Layout Object
FieldTypeDefaultDescription
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.
#Layout Example
// 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" }
#Limits
LimitValue
html sizeMax 500 KB
signersMax 6 per document
PDF pagesMax 100 pages for signing/stamping
PDF file sizeMax 20 MB for signing/stamping
Rate limit100 requests/hour per authenticated user · 20/hour per IP for shared keys
#Template Example
// 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
}
#Response — 201 Created
"id":     "4b860f6c...",       // Document ID
"url":    "https://zipzign.com/doc/4b860f6c...",
"status": "hosted" // | "awaiting_signatures" | "awaiting_payment" | "draft"
#Response Codes
201Document created successfully.
400Invalid JSON, missing required fields, unrecognized type, invalid layout values, oversized html (>500 KB), or Handlebars template syntax error.
401Missing or invalid API key.
429Rate limit exceeded (100/hour per user, 20/hour per IP).
502PDF generation or Stripe payment intent creation failed.
#Example — Readable
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>"
  }'
#Example — Signable
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" }
    ]
  }'
#Example — Payable (USD)
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" }
  }'
#Example — Payable (EUR)
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" }
  }'
#Example — Sign-Then-Pay
// 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
GET /api/documents List documents
🔑 Requires API key

Returns a paginated list of documents belonging to the API key owner.

#Query Parameters
ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger20Results per page (max 100)
statusstringFilter: hosted, awaiting_signatures, awaiting_payment, all_signed, paid
typestringFilter: readable, signable, payable
created_fromstringISO date start (e.g. 2026-01-01)
created_tostringISO date end
sortstringdescasc or desc by created_at
sandboxstringfalseSet true to list sandbox documents only. Note: sandbox documents expire after 24 hours.
#Example
curl "https://zipzign.com/api/documents?page=1&type=signable" \
  -H "Authorization: Bearer <KEY>"
#Response
{
  "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"
    }
  ]
}
GET /api/documents/:id Get document

Returns full document metadata, a short-lived PDF URL, and Stripe payment fields (for payable documents awaiting payment).

#Path Parameters
ParameterTypeDescription
idstringDocument ID returned from POST /api/documents.
#Response — 200 OK
{
  "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
}
#Response Codes
200Document found.
404Document not found.
GET /api/documents/:id/status Get status

Lightweight status check. Useful for polling without fetching the full document.

#Response — 200 OK
{
  "status":    "awaiting_signatures",
  "updated_at": "2026-04-07T18:30:36Z"
}
#Response Codes
200Status returned.
404Document not found.
PUT /api/documents/:id Update document
🔑 Requires API key

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.

#Request Body (JSON) — exactly one input source required
FieldTypeRequiredDescription
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.

#Response — 200 OK (async html/template example)
{
  "id":      "4b860f6c...",
  "type":    "signable",
  "status":  "created",
  "pdf_url": "https://zipzign.com/pdf/4b860f6c...",
  "doc_url": "https://zipzign.com/doc/4b860f6c..."
}
#Response Codes
200Document update accepted. Poll doc_url for final status (html/template inputs).
400No input source provided, invalid data, invalid layout, or template render error.
401Missing or invalid API key.
404Document or template not found.
409Document has already been paid, signed, or completed.
429Daily edit budget exhausted (code: "EDIT_LIMIT"). See retry_after_seconds.
PATCH /api/documents/:id Append pages to document
🔑 Requires API key

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

#Request Body (JSON) — exactly one input source required
FieldTypeRequiredDescription
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.
#Response — 200 OK
{
  "id":      "4b860f6c...",
  "type":    "readable",
  "status":  "hosted",
  "pdf_url":  "https://zipzign.com/pdf/4b860f6c...",
  "amount":  null
}
#Response Codes
200Pages appended; base PDF updated in place.
400No input provided, multiple inputs provided, invalid layout, invalid data, or html too large.
401Missing or invalid API key.
404Document not found, or template_id not found.
409Document status is not hosted or draft (signable/payable docs are immutable once sent).
429Daily edit budget exhausted (code: "EDIT_LIMIT"). See retry_after_seconds.
502PDF generation, fetch, or merge failed.
POST /api/documents/:id/send Send a draft document
🔑 Requires API key

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.

#Response — 200 OK
{
  "id":      "4b860f6c...",
  "type":    "signable",
  "status":  "awaiting_signatures",
  "pdf_url":  "https://zipzign.com/pdf/4b860f6c...",
  "amount":  null
}
#Response Codes
200Document activated; emails sent / payment intent created.
401Missing or invalid API key.
404Document not found.
409Document is not in draft status. Only draft documents can be sent.
502Stripe payment intent creation failed (payable documents only).
#Example
curl -X POST https://zipzign.com/api/documents/4b860f6c.../send \
  -H "Authorization: Bearer <KEY>"
POST /api/documents/:id Add signing or payment phase
🔑 Requires API 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.

#Prerequisites

Document must be in hosted or complete status. Documents that are mid-signing, mid-payment, deleted, or in error state return 409.

#Request Body — Add Signatures
FieldTypeRequiredDescription
signers array required Array of { email: string, name?: string }. Max 20.
notify_emails string[] optional Additional emails to notify on completion.
#Request Body — Add Payment
FieldTypeRequiredDescription
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.
#Example — Add signatures to a readable document
// 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" }
    ]
  }'
#Example — Add payment after signing completes
// 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"
  }'
#Response Codes
200Phase added. Emails sent, document transitioned.
400Invalid body, missing fields, or both signers and payer provided.
401Missing or invalid API key.
404Document not found.
409Document is not in hosted or complete status.
502Stripe payment intent creation failed.
DELETE /api/documents/:id Delete document

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.

#Response — 200 OK
{ "success": true }
#Response Codes
200Document deleted.
401Missing or invalid API key.
404Document not found.
409Document has already been paid or signed and cannot be deleted.
POST /api/documents/bulk-send Bulk send to multiple recipients

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

#Request Body
FieldTypeRequiredDescription
recipientsarrayrequiredArray of { email: string, name?: string }. Max 20 per call.
template_idstringoptional*Template ID to render for each document. Mutually exclusive with html.
htmlstringoptional*Raw HTML source. Either template_id or html is required.
dataobjectoptionalTemplate variables applied to every document.
messagestringoptionalCustom message included in every signer invite email.
notify_emailsarrayoptionalEmail addresses to notify when each document is signed.
sandboxbooleanoptionalCreate sandbox documents (no real emails, auto-deleted after 24 h).
#Response — 201 Created
{
  "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" }
  ]
}
#Response Codes
201Documents created. PDF generation and emails are processed asynchronously.
400Validation error (missing recipients, too many, invalid email, etc.).
401Missing or invalid API key.
404Template not found.
POST /api/user/documents/:id/remind Send signing reminders

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.

#Response — 200 OK
{
  "ok": true,
  "reminded": [
    { "email": "alice@co.com", "name": "Alice" }
  ]
}
#Response Codes
200Reminders sent.
400Document is not a signable document.
404Document not found.
409All signers have already signed.
429Signers were reminded recently — wait at least 1 hour.
POST /api/user/documents/:id/mark-paid Mark a payable document as paid (without Stripe)

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.

#Request Body (JSON, optional)
FieldTypeRequiredDescription
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.
#Response — 200 OK
{
  "ok": true,
  "status": "paid",
  "method": "wire",
  "stripe_payment_intent_canceled": true
}
#Response Codes
200Document marked paid; webhooks fired.
400Document is not a payable document.
404Document not found.
409Document is already paid or not in a payable state.
POST /api/documents/:id/sign Submit signature

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.

🔐 ESIGN Act compliance (SES): ZipZign captures affirmative signer consent, timestamp, IP address, and user agent on every submission. A Certificate of Completion with the full audit trail is appended to the signed PDF. A SHA-256 hash provides tamper evidence verifiable at 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.
#Query Parameters
ParameterTypeRequiredDescription
token string required Signer-specific token from the invitation email URL.
#Request Body (JSON)
FieldTypeRequiredDescription
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.
#Limits
LimitValue
signature_data_urlMax 200 KB
Rate limit10 attempts / 5 minutes per IP
#Response — 200 OK
{
  "success":   true,
  "all_signed": false  // true when every signer has submitted
}
#Response Codes
200Signature accepted.
400Missing token, invalid JSON, or invalid signature_data_url format.
404Token not found or document does not exist.
409This signer has already submitted a signature.
413signature_data_url exceeds 200 KB.
429Rate limit exceeded (10 attempts / 5 min per IP).
GET /api/documents/:id/verify Verify document integrity

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.

#Response — 200 OK
{
  "valid":        true,
  "document_id":   "abc123",
  "stored_hash":   "e3b0c44298fc1c14...",
  "computed_hash": "e3b0c44298fc1c14..."
}
#Response Codes
200Hash comparison result returned.
404Document not found or has not been signed yet.
GET /api/documents/:id/audit Audit trail

Returns the full signing audit trail for a document: signer details (consent, IP, user agent) and chronological signing events. Requires API key authentication.

#Response — 200 OK
{
  "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..."
  }]
}
#Response Codes
200Audit trail returned.
404Document not found.
GET /pdf/:id Serve PDF

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.

#Response Headers
HeaderValue
Content-Typeapplication/pdf
Content-Dispositioninline; filename="document-<id>.pdf"
X-Frame-OptionsALLOWALL — safe to embed in iframes
Cache-Controlpublic, max-age=3600
#Response Codes
200PDF bytes returned directly.
404Document not found or PDF not yet generated.
500Failed to read PDF from storage.
#Embedding example
<iframe
  src="https://zipzign.com/pdf/4b860f6c..."
  width="100%"
  height="800"
  style="border:none"
/>
Templates
POST /api/templates Create template
🔒 Requires session (dashboard login)

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.

#Request Body (JSON)
FieldTypeRequiredDescription
namestringrequiredTemplate name (max 255 chars)
htmlstringrequiredHTML with {{variable}} placeholders (max 500 KB). Supports full Handlebars syntax — {{#each}}, {{#if}}, helpers, etc.
#Example
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>"
  }'
Response (201)
{
  "id":         "tpl_abc123...",
  "name":       "Invoice Template",
  "variables":  ["client_name", "total"],  // auto-detected from {{...}} placeholders
  "created_at": "2026-04-09T12:00:00.000Z"
}
#Using a template to create a document
// 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" }
}
GET /api/templates List templates
🔒 Requires session

Returns all templates belonging to the authenticated user.

GET /api/templates/:id Get template
🔒 Requires session

Returns the full template including HTML source and detected variables.

PUT /api/templates/:id Update template
🔒 Requires session

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.

#Request Body (JSON)
FieldTypeRequiredDescription
namestringoptionalNew template name
htmlstringoptionalNew HTML source with {{variable}} placeholders
DELETE /api/templates/:id Delete template
🔒 Requires session

Permanently delete a template. Existing documents created from this template are not affected.

#Response Codes
200{ "ok": true }
404Template not found or not owned by user.

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

Overview

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.

#Example payload
{
  "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" }
    ]
  }
}
#Document fields
FieldTypeDescription
doc_urlstringPublic URL to view/sign the document.
pdf_urlstring | nullDirect URL to download the PDF. null if no PDF exists yet.
amountnumber | nullPayment amount in smallest currency unit (cents). null for non-payable documents.
currencystringThree-letter ISO currency code.
payer_emailstring | nullPayer's email address, if applicable.
payer_namestring | nullPayer's name, if provided.
sandboxbooleanWhether this is a sandbox/test document.
created_atstringISO 8601 creation timestamp.
updated_atstringISO 8601 last-updated timestamp.
metadataarrayCustom key/value pairs, if set. Omitted when empty.
signersarraySigner list with email, name, and signed_at. Only present for signable documents.
#Request headers
HeaderDescription
Content-Typeapplication/json
X-ZipZign-SignatureHMAC-SHA256 signature — see Signature Verification below
User-AgentZipZign-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.

Delivery Behavior
ScenarioWhat 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.

Events
EventFired 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.
Signature Verification

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.

#Node.js verification example
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));
}
POST /api/user/webhooks Add webhook endpoint
🔒 Requires session

Register a new webhook endpoint. Returns the signing secret — store it securely. Up to 10 endpoints per account.

#Request Body (JSON)
FieldTypeRequiredDescription
urlstringrequiredThe HTTPS URL to receive events
labelstringoptionalFriendly name, e.g. "Production" or "Zapier"
enabledbooleanoptionalWhether to deliver events. Defaults to true
#Response (201)
{
  "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"
}
GET /api/user/webhooks List webhook endpoints
🔒 Requires session

Returns all registered endpoints for the authenticated user, including their signing secrets.

PATCH /api/user/webhooks/:id Update webhook endpoint
🔒 Requires session

Update the URL, label, or enabled state of an endpoint. The signing secret is preserved when the URL changes.

#Request Body (JSON — all fields optional)
FieldTypeDescription
urlstringNew endpoint URL
labelstringNew friendly name
enabledbooleanEnable or disable delivery
DELETE /api/user/webhooks/:id Delete webhook endpoint
🔒 Requires session

Permanently removes the endpoint and its signing secret.

#Response Codes
200{ "ok": true }
404Endpoint not found or not owned by user.
POST /api/user/webhooks/:id/test Send test event
🔒 Requires session

Immediately fires a webhook.test event to the specified endpoint. Useful for verifying your integration is working correctly.

#Response Codes
200{ "ok": true, "url": "https://..." }
400No webhook configured.
502Delivery to your endpoint failed.

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.

GET /api/user/email-domain Get current domain config

Returns the current custom email domain for the organization, or null if none is configured.

FieldTypeDescription
idstringInternal domain ID (from Resend)
domainstringVerified domain (e.g. acme.com)
from_emailstringFull from address (e.g. noreply@acme.com)
from_namestring|nullDisplay name in the From header
statusstringpending | verified | failed
dns_recordsarray|nullDNS records to add. Each record has type, name, value, status.
created_atstringISO 8601 timestamp

Auth: Session required. Tier: Any (returns null if unpaid).

POST /api/user/email-domain Register a custom domain

Registers a domain with the email provider and returns DNS records to add. The domain remains pending until DNS records are added and verified.

ParameterTypeRequiredDescription
domainstringYesDomain to verify, e.g. acme.com
from_emailstringYesFull from address — must end with @{domain}
from_namestringNoDisplay 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"
}
POST /api/user/email-domain/verify Re-check DNS verification

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.

DELETE /api/user/email-domain Remove custom domain

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.

Roles
RoleDescription
ownerFull access. Can rename the workspace, manage billing, change any member's role, and remove other owners (as long as at least one owner remains).
adminCan invite, remove, and manage member/viewer roles. Cannot remove owners or access billing.
memberCan create and send documents. Cannot manage other members.
viewerRead-only access to documents.
X-Org-Id header

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.

GET /api/orgs/{orgId}/members List members of a workspace

Requires session auth. Any member of the workspace may call this.

#Response
[
  {
    "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
  }
]
POST /api/orgs/{orgId}/invitations Invite a member by email

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.

#Body
FieldTypeDescription
email requiredstringEmail address to invite.
rolestringRole to grant on acceptance: owner, admin, member (default), or viewer. Admins can only invite member or viewer.
Response 201 Created
{
  "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…"
}
POST /api/invitations/{token}/accept Accept an invitation

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.

Response 200 OK
{ "ok": true, "org_id": "org_abc123", "role": "member" }

Returns 410 Gone if the invitation has been revoked, already accepted, or expired.

PATCH /api/orgs/{orgId}/members/{memberId} Change a member's role

Requires owner role. Cannot demote the last owner.

#Body
FieldTypeDescription
role requiredstringowner, admin, member, or viewer.
DEL /api/orgs/{orgId}/members/{memberId} Remove a member (or leave)

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.

POST /api/orgs Create a new workspace

Creates a non-personal workspace and makes the caller its owner. The new workspace starts on the free plan with its own independent quota.

#Body
FieldTypeDescription
name requiredstringDisplay name for the workspace. Max 255 characters.
Response 201 Created
{ "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.

MCP Server (Model Context Protocol)

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.

#Install (Claude Code)
claude mcp add zipzign https://zipzign.com/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"
#Base URL
https://zipzign.com/mcp

Transport: Streamable HTTP (stateless, no session management). Auth: existing ZipZign API key passed via Authorization: Bearer ds_… header.

#Available tools
ToolDescription
meVerify the API key; return email and user ID.
list_documentsList documents with optional type/status filters and pagination.
get_documentFetch full document state including signers, URLs, and metadata.
create_documentCreate a readable, signable, or payable document.
update_documentReplace document content (HTML / template / PDF URL). Includes dedup + daily edit cap.
append_documentAppend HTML pages to an existing draft document.
send_documentSend a draft — triggers signing/payment emails.
delete_documentSoft-delete a document.
replace_signerSwap an unsigned signer atomically; sends a fresh invite to the new email.
get_audit_trailRetrieve the full audit log for compliance and debugging.
#Example prompts
  • "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?"
💡 Tip: say "sandbox" out loud

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.

Zapier Zapier

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.

#Triggers
  • New Signed Document
  • New Paid Document
  • Document Status Changed
  • New Document
#Actions
  • 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 Make.com

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.

#Typical setup
  1. Add an HTTP module. Set URL to https://zipzign.com/api/documents, method POST.
  2. Under Headers, add Authorization: Bearer ds_….
  3. Set body type to JSON and map fields from your upstream trigger.
  4. 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.

GET /api/user/account/export Export workspace data

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.

Response structure
{
  "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" }
}
POST /api/user/erasure Erase subject data (GDPR Art. 17)

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.

FieldTypeDescription
emailstringRequired. Email address of the data subject.
reasonstringOptional. Free-text reason for the erasure request (kept in your audit trail).
Request
POST /api/user/erasure
{ "email": "alice@example.com", "reason": "GDPR request received 2024-01-15" }
Response
{ "ok": true, "erased_email": "alice@example.com", "signer_records_anonymized": 3, "payer_records_anonymized": 1, "total_anonymized": 4 }
200Erasure complete. Returns counts of anonymized records (may be 0 if email not found).
400email field missing.
403Caller does not have admin role or higher.
GET /api/user/account/retention Get retention policy

Returns the workspace's current document retention policy.

Response
{ "retention_days": 365, "description": "Documents older than 365 days are automatically soft-deleted." }

retention_days: null means documents are retained indefinitely (default).

PUT /api/user/account/retention Set retention policy

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.

FieldTypeDescription
retention_daysinteger | nullDays to retain documents. Must be ≥ 30. Pass null to disable.
Request — set 1-year retention
PUT /api/user/account/retention
{ "retention_days": 365 }
Request — clear policy
PUT /api/user/account/retention
{ "retention_days": null }
DELETE /api/user/account Delete account

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.

FieldTypeDescription
confirmstringRequired. Must be the exact string "DELETE MY ACCOUNT".
Request
DELETE /api/user/account
{ "confirm": "DELETE MY ACCOUNT" }
Response
204 No Content
204Account deleted. Session cookie cleared.
400confirm field missing or incorrect, or called from a shared workspace.
403Caller does not have owner role.
409Active paid subscription exists — cancel it first.