SendSignatures API v1

Embed e‑signature
into your application.

Fill any template with your app's data, preview it, and send for signature — with idempotent drafts, your own reference IDs, HMAC‑signed webhooks, and executed‑document retrieval. Customers sign on SendSignatures; your users never leave your product.

Quickstart Get an API key

Basics

Every request is JSON over HTTPS with a bearer key.

Base URLhttps://sendsignatures.com/send/v1  (the unversioned /send prefix is an alias)
AuthAuthorization: Bearer <YOUR_API_KEY> on every request. Missing/invalid ⇒ 401.
Content typeContent-Type: application/json on POST/PATCH.
IdempotencySend Idempotency-Key: <uuid> on any POST (/batch, /confirm). Re‑sending the same key returns the original response — never a duplicate.
Correlationexternal_reference (string) + metadata (object), at batch and per‑document level, are echoed back in every response, status call, and webhook.
Rate limits300 req/min per key. Every response carries X-RateLimit-Limit/-Remaining/-Reset; over the limit ⇒ 429 + Retry-After.
ErrorsAlways { "error": { "code", "message", "details": [] } } with a conventional HTTP status. See Errors.
Draft lifecycle. /batch creates drafts — nothing is emailed. Preview, PATCH, and DELETE them freely. /confirm is the only call that emails signers. Unconfirmed drafts auto‑expire (default 48h) and are cleaned up.

Quickstart

The whole flow in five calls. Set KEY to your API key first.

# 1 — See which fields a template needs (do this once per template)
curl -s https://sendsignatures.com/send/v1/templates/6/fields \
  -H "Authorization: Bearer $KEY"

# 2 — Create a pre-filled DRAFT (no email is sent). Keep the returned submission_id.
curl -s -X POST https://sendsignatures.com/send/v1/batch \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2b9e-1a2b-4c3d-8e9f-000000000001" -d '{
    "sender_name": "Jane Rep",
    "external_reference": "deal-9001",
    "signers": { "Customer": { "name": "Sam Homeowner", "email": "sam@example.com" } },
    "documents": [
      { "template": "Install Agreement",
        "data": { "Customer Phone": "555-1212", "Address": "456 Solar Rd" } }
    ]
  }'
# -> { "batch_id": "...", "documents": [ { "submission_id": 42, "preview_url": "...", ... } ] }

# 3 — Preview the filled PDF in your UI (still nothing emailed)
curl -s https://sendsignatures.com/send/v1/preview/42 \
  -H "Authorization: Bearer $KEY" -o preview.pdf

# 4 — Send it for signature
curl -s -X POST https://sendsignatures.com/send/v1/confirm \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "submission_ids": [42] }'

# 5 — Get a webhook when it's signed (register your URL once — see Webhooks)

Templates

GET/templates

List available templates.

curl -s https://sendsignatures.com/send/v1/templates -H "Authorization: Bearer $KEY"

// -> 200
[ { "id": 6, "name": "Install Agreement", "roles": ["Brighthouse Representative","Customer","HIS Signer"] } ]
GET/templates/{id}/fields

The fields on a template. Supply the prefillable ones in /batchdata (keyed by name). Fields owned by a signer role are filled by that signer at signing time, not by you.

curl -s https://sendsignatures.com/send/v1/templates/6/fields -H "Authorization: Bearer $KEY"

// -> 200
{ "template": "Install Agreement", "template_id": 6,
  "roles": [ { "name": "Brighthouse Representative", "is_requester": true },
             { "name": "Customer", "is_requester": false } ],
  "fields": [
    { "name": "Customer Phone", "label": "Customer Phone", "type": "text", "data_type": "text",
      "format": null, "required": false, "prefillable": true, "role": "Brighthouse Representative" },
    { "name": "Customer Signature", "label": "Customer Signature", "type": "signature",
      "format": null, "required": true, "prefillable": false, "role": "Customer" }
  ] }

PowerForms — sign from a link

A public, per‑template link a customer opens to review and sign. No per‑signer orchestration: mint a link, embed it, done. Reach for this when you just want a "Sign now" button; use Drafts when you want a preview/confirm step inside your own UI first.

POST/powerform-links

Mint a send‑enabled URL for a template, optionally pre‑filled. Drop the returned url behind a button. The link carries a signed send‑token, so opening it needs no API key.

Field
templateTemplate name or id.
modeblank (the opener enters their own details) or full (you pre‑fill data + recipient). Default blank.
recipients{ "<role>": { "name", "email" } } — who signs, keyed by signer role.
prefill{ "<Field Name>": value } — sender‑side field values (used with mode: full). Keys are field names from /templates/{id}/fields.
expires_in_hoursLink TTL. Default 720 (30 days), max 8760 (1 year).
curl -s -X POST https://sendsignatures.com/send/v1/powerform-links \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
    "template": "Home Improvement Agreement",
    "mode": "full",
    "expires_in_hours": 720,
    "recipients": { "Customer": { "name": "Sam Homeowner", "email": "sam@example.com" } },
    "prefill": { "Property Address": "456 Solar Rd", "Total Solar Cost": "28500" }
  }'

// -> 200
{ "url": "https://sendsignatures.com/f/abc123?t=…", "template": "Home Improvement Agreement",
  "mode": "full", "expires_at": "2027-07-18T00:00:00Z" }
GET POST/f/{slug} · public · token‑gated

The public form. Open the minted url as‑is for a blank send (the opener types who should sign), or append mode=full + prefill params to drive it entirely from your app. On submit, each signer is emailed a link to sign.

# Blank — embed exactly as returned; the opener enters the signer email and sends
https://sendsignatures.com/f/abc123?t=<token>

# Pre-filled from your app — append recipient + field data (URL-encode the values)
https://sendsignatures.com/f/abc123?t=<token>&mode=full
   &r[Customer][name]=Sam%20Homeowner
   &r[Customer][email]=sam@example.com
   &d[Property%20Address]=456%20Solar%20Rd

# Fully server-driven (no intermediate page): POST the same params form-encoded
curl -s -X POST https://sendsignatures.com/f/abc123 \
  --data-urlencode "t=<token>" --data-urlencode "mode=full" \
  --data-urlencode "r[Customer][email]=sam@example.com" \
  --data-urlencode "d[Property Address]=456 Solar Rd"
Signing order is enforced by the template, not your code. Where a template bundles a prerequisite document (e.g. the CPUC solar disclosure) ahead of the deal signature with required fields, the signer cannot reach or submit the later signature until the earlier one is complete — it is one sequential, required‑field flow.

Drafts: create, preview, edit, send

POST/batch · Idempotency-Key supported

Fill one or more templates and create drafts. No email is sent. Returns a submission_id + preview_url per document.

Request body

Field
sender_nameName shown as the sender (the requester role is auto‑filled + marked complete).
signers{ "<role>": { "name", "email", "data": {…} } } — one entry per signer role. data is optional per‑signer field values.
documents[]Each: template (name or id), data (field name → value), optional external_reference, metadata, signing_order (sequential default, or parallel), and a per‑document signers override (for mixed‑template batches with different roles).
external_reference, metadataYour correlation ids, echoed everywhere. Batch‑level and/or per‑document.
cc[{ "name", "email" }] — CC recipients receive the completed document (not the initial invite).
expires_in_hoursDraft TTL before auto‑cleanup (default 48, max 168).
dry_runtrue = validate only, create nothing.
curl -s -X POST https://sendsignatures.com/send/v1/batch \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2b9e-…-0001" -d '{
    "sender_name": "Jane Rep",
    "external_reference": "deal-9001",
    "metadata": { "deal_id": 9001 },
    "cc": [{ "name": "Ops", "email": "ops@yourco.com" }],
    "expires_in_hours": 48,
    "signers": { "Customer": { "name": "Sam Homeowner", "email": "sam@example.com" } },
    "documents": [
      { "template": "Install Agreement", "external_reference": "doc-1",
        "signing_order": "sequential",
        "data": { "Customer Phone": "555-1212", "Address": "456 Solar Rd" } }
    ]
  }'

// -> 200
{ "batch_id": "958a3da1-…", "external_reference": "deal-9001", "metadata": { "deal_id": 9001 },
  "expires_at": "2026-07-10T19:05:50Z",
  "documents": [ {
    "template": "Install Agreement", "submission_id": 42, "batch_id": "958a3da1-…",
    "external_reference": "doc-1", "metadata": null,
    "preview_url": "https://sendsignatures.com/send/preview/42",
    "expires_at": "2026-07-10T19:05:50Z",
    "submitters": [ { "id": 21, "role": "Customer", "email": "sam@example.com",
                      "status": "awaiting", "sign_link": "https://sendsignatures.com/s/abc" } ],
    "unmapped_data_keys": [], "missing_required_fields": [] } ] }
Validate before you send. unmapped_data_keys are keys you sent that no field matched; missing_required_fields are required prefillable fields you left blank. Use dry_run:true to check without creating anything.
GET/preview/{submission_id}

The filled document, reflecting the latest PATCH. Safe — nothing is emailed. Default is the full PDF; add ?format=png&page=N (optional &width=, default 1200px) for a single page as a PNG to render inline. GET /submissions/{id} returns page_count.

# full PDF
curl -s "https://sendsignatures.com/send/v1/preview/42" -H "Authorization: Bearer $KEY" -o preview.pdf
# page 1 as PNG
curl -s "https://sendsignatures.com/send/v1/preview/42?format=png&page=1" -H "Authorization: Bearer $KEY" -o page1.png
PATCH/submissions/{id}

Edit an unconfirmed draft in place — same id. Merge new field values and/or change signer name/email. 409 if already sent.

curl -s -X PATCH https://sendsignatures.com/send/v1/submissions/42 \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
    "data": { "Customer Phone": "555-9999" },
    "signers": { "Customer": { "email": "sam.new@example.com" } }
  }'
// -> 200  { "submission_id": 42, "status": "draft", "signers": [ … ], … }
DELETE/submissions/{id}

Void one unconfirmed draft. Idempotent — a no‑op if already gone; 409 if already sent.

curl -s -X DELETE https://sendsignatures.com/send/v1/submissions/42 -H "Authorization: Bearer $KEY"
// -> 200  { "deleted": true, "submission_id": 42 }
DELETE/batches/{batch_id}

Void every unconfirmed draft in a batch. Already‑sent ones are left alone and reported.

curl -s -X DELETE https://sendsignatures.com/send/v1/batches/958a3da1-… -H "Authorization: Bearer $KEY"
// -> 200  { "deleted": true, "batch_id": "958a3da1-…",
//         "deleted_submission_ids": [42], "skipped_sent_ids": [] }
POST/confirm · Idempotency-Key supported

Dispatch the signing request(s) after your user confirms. This is the step that emails signers.

curl -s -X POST https://sendsignatures.com/send/v1/confirm \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "submission_ids": [42, 43] }'

// -> 200
{ "sent": [ { "submission_id": 42, "sent_to": "sam@example.com", "role": "Customer", "test_mode": false } ],
  "skipped": [ { "submission_id": 43, "reason": "already_sent" } ] }

Status & executed documents

GET/submissions/{id}   GET/batches/{batch_id}

Reconcile at any time (fallback if you miss a webhook). Returns status, per‑signer progress, timestamps, page_count, and your external_reference/metadata.

curl -s https://sendsignatures.com/send/v1/submissions/42 -H "Authorization: Bearer $KEY"

// -> 200
{ "submission_id": 42, "batch_id": "958a3da1-…", "external_reference": "doc-1", "metadata": null,
  "status": "completed", "template": "Install Agreement",
  "expires_at": null, "page_count": 4,
  "signers": [ { "role": "Customer", "name": "Sam Homeowner", "email": "sam@example.com",
                 "status": "completed", "completed_at": "2026-07-08T20:11:00Z" } ] }

Statuses: draftsentopenedcompleted (or declined / voided).

GET/submissions/{id}/document

The executed (signed) PDF. 409 until completed.

curl -s https://sendsignatures.com/send/v1/submissions/42/document -H "Authorization: Bearer $KEY" -o signed.pdf
GET/submissions/{id}/certificate

The signing audit certificate (ESIGN/UETA trail). 409 until completed.

curl -s https://sendsignatures.com/send/v1/submissions/42/certificate -H "Authorization: Bearer $KEY" -o certificate.pdf
The submission.completed webhook also carries short‑lived document_url + certificate_url so you rarely need these directly.

Historical DocuSign archive

Signed documents that were executed in DocuSign before the migration to SendSignatures. They are not DocuSeal submissions and will never appear under /submissions — they are a separate read-only archive on disk, reachable with the same Bearer key.

Completed envelopes only. The DocuSign export captured completed envelopes exclusively — 1,374 of them. Voided and declined envelopes, and DocuSign template definitions, were deliberately not migrated and are not retrievable through this API. If an envelope you expect is absent, it was almost certainly never archived rather than lost. Every list response repeats this in a scope object so an integration can assert it programmatically.
GET/archive

What the archive holds, and the per-customer breakdown. files_present counts indexed documents whose PDF is actually on disk — it should equal total_documents.

curl -s https://sendsignatures.com/send/v1/archive -H "Authorization: Bearer $KEY"

// -> 200
{ "total_documents": 1374, "total_customers": 412, "files_present": 1374,
  "scope": { "archived_statuses": ["completed"],
             "excluded_statuses": ["voided","declined"],
             "note": "Only COMPLETED DocuSign envelopes were archived. …" },
  "customers": [ { "name": "Acme Solar", "documents": 6, "internal": false } ] }
GET/archive/customers

Start here when you only know a customer’s name. Customer names are free text carried over from DocuSign, so exact matches are unreliable. Search names with q (case-insensitive substring), then use the exact name you get back with ?customer= below. Ordered by document count, descending.

curl -s "https://sendsignatures.com/send/v1/archive/customers?q=acme" -H "Authorization: Bearer $KEY"

// -> 200
{ "total": 2, "limit": 100, "offset": 0, "count": 2,
  "scope": { "archived_statuses": ["completed"], … },
  "customers": [ { "name": "Acme Solar LLC", "documents": 6, "internal": false },
                 { "name": "Acme Solar (old)", "documents": 1, "internal": false } ] }
GET/archive/documents

List archived documents, newest first. Filter with customer (exact name), customer_like (case-insensitive substring on the customer name only — the usual way to search by customer), or q (substring spanning subject, customer and envelope id). Paginate with limit (default 100, max 500) and offset.

curl -s "https://sendsignatures.com/send/v1/archive/documents?customer_like=acme&limit=2" -H "Authorization: Bearer $KEY"

// -> 200
{ "total": 37, "limit": 2, "offset": 0, "count": 2,
  "scope": { "archived_statuses": ["completed"], … },
  "documents": [ { "envelope_id": "97f4e228-fbef-4d51-b65a-84e4ca35fd3b",
                   "date": "2022-09-14", "subject": "Install Agreement",
                   "customer": "Acme Solar", "method": "email",
                   "filename": "Acme_Solar_2022-09-14_Install_Agreement.pdf",
                   "available": true,
                   "download_url": "/send/v1/archive/documents/97f4e228-…/download" } ] }
GET/archive/documents/{envelope_id}

Metadata for one archived document. 404 if that envelope id was never archived.

curl -s https://sendsignatures.com/send/v1/archive/documents/97f4e228-fbef-4d51-b65a-84e4ca35fd3b \
  -H "Authorization: Bearer $KEY"
GET/archive/documents/{envelope_id}/download

The signed PDF as executed in DocuSign, including its Certificate of Completion. 404 if never archived; 410 if indexed but the file is missing from disk; 503 if the archive volume is not mounted.

curl -s https://sendsignatures.com/send/v1/archive/documents/97f4e228-fbef-4d51-b65a-84e4ca35fd3b/download \
  -H "Authorization: Bearer $KEY" -o archived.pdf
Humans can browse the same archive at /archive in the web UI (session login, grouped per customer, with per-customer ZIP download). The API and the browser read the identical on-disk index.

Webhooks

Register your callback once, then verify the HMAC signature on every delivery.

1. Register your callback URL

POST/webhooks

Point events at your endpoint. Returns your signing secret the first time (or when you pass rotate_secret:true). If your key already has a secret (e.g. one was issued to you at onboarding), a plain call keeps it and returns secret_set:true without re‑showing it — pass rotate_secret:true to get a fresh one.

curl -s -X POST https://sendsignatures.com/send/v1/webhooks \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
    "url": "https://yourapp.com/hooks/sendsignatures",
    "events": ["submission.completed","submission.signed","submission.declined"]
  }'

// -> 200  (events is optional; omit it to receive all)
{ "url": "https://yourapp.com/hooks/sendsignatures",
  "events": ["submission.completed","submission.signed","submission.declined"],
  "secret_set": true,
  "secret": "whsec_…",   // shown once — store it now
  "signing": "X-SendSignatures-Signature: sha256=HMAC_SHA256(secret, \"{timestamp}.{raw_body}\"); X-SendSignatures-Timestamp: {unix}" }
GET/webhooks   DELETE/webhooks

GET shows the current config (secret hidden). DELETE stops delivery.

curl -s https://sendsignatures.com/send/v1/webhooks -H "Authorization: Bearer $KEY"
// -> { "url": "https://yourapp.com/hooks/sendsignatures", "events": [...], "secret_set": true, "signing": "…" }

2. Events

EventWhen
submission.createdA draft was created.
submission.viewedA signer opened the document.
submission.signedA signer finished their part (per‑signer, on multi‑signer docs).
submission.completedAll signers done. Includes documents.document_url + certificate_url.
submission.declinedA signer declined.
submission.voidedVoided or expired.

3. Delivery format

POST https://yourapp.com/hooks/sendsignatures
X-SendSignatures-Event: submission.completed
X-SendSignatures-Event-Id: 5f2c8a01-…      # unique per delivery — dedupe on this
X-SendSignatures-Timestamp: 1789000000
X-SendSignatures-Signature: sha256=<hex>
Content-Type: application/json

{ "event_id": "5f2c8a01-…", "event_type": "submission.completed",
  "occurred_at": "2026-07-08T19:20:00Z",
  "submission_id": 42, "batch_id": "958a3da1-…",
  "external_reference": "doc-1", "metadata": { "deal_id": 9001 },
  "status": "completed",
  "signers": [ { "role": "Customer", "name": "Sam", "email": "sam@example.com",
                 "status": "completed", "completed_at": "2026-07-08T19:19:58Z" } ],
  "documents": { "document_url": "https://…/signed.pdf", "certificate_url": "https://…/cert.pdf" },
  "timestamps": { "created_at": "…", "completed_at": "…", "expires_at": null } }

4. Verify the signature

Compute HMAC‑SHA256 over the string "{timestamp}.{raw_body}" with your secret, compare (constant‑time) to X-SendSignatures-Signature, and reject if the timestamp is more than 5 minutes old.

// Node.js (Express)
const crypto = require('crypto');
function verify(req, secret) {
  const ts  = req.header('X-SendSignatures-Timestamp');
  const sig = req.header('X-SendSignatures-Signature');      // "sha256=…"
  const raw = req.rawBody;                                    // the exact bytes received
  if (Math.abs(Date.now()/1000 - Number(ts)) > 300) return false;   // replay guard
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + raw).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
# Python (Flask)
import hmac, hashlib, time
def verify(headers, raw_body, secret):
    ts  = headers["X-SendSignatures-Timestamp"]
    sig = headers["X-SendSignatures-Signature"]              # "sha256=…"
    if abs(time.time() - int(ts)) > 300: return False        # replay guard
    expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
Retries & delivery. Return 2xx to acknowledge. Non‑2xx (or timeout) is retried with exponential backoff — 2^n minutes, up to 8 attempts. Delivery is at‑least‑once: dedupe on event_id. bounced/failed is best‑effort (derived from our mail log).

Sandbox / test mode

Build against a test‑mode API key: it behaves exactly like a live key — /batch, /preview, /confirm all work — but no real emails are ever sent.

POST/submissions/{id}/simulate · test keys only

Marks a submission completed and fires the submission.completed webhook (with document + certificate links), so you can exercise your webhook handler and document retrieval without a human signing. Returns 403 for live keys.

# with your SANDBOX key: create -> confirm -> simulate -> receive the completed webhook
curl -s -X POST https://sendsignatures.com/send/v1/submissions/42/simulate -H "Authorization: Bearer $TEST_KEY"
// -> 200  { "submission_id": 42, "status": "completed", "simulated": true, … }

Errors & status codes

Every error is the same shape:

{ "error": { "code": "conflict", "message": "Draft already sent; cannot edit", "details": [] } }
StatuscodeMeaning
400/422invalid_requestMalformed body or missing required input (details may list fields).
401unauthorizedMissing/invalid API key.
403forbiddenNot allowed (e.g. simulate on a live key).
404not_foundSubmission/batch/template not found or not owned by your key.
409conflictState conflict — e.g. editing/deleting an already‑sent draft, or fetching a document before completion.
429rate_limitedOver 300/min. Honor Retry-After.

Get access

Keys are issued per application (a live key and a sandbox key). Contact the SendSignatures administrator to request keys and confirm your webhook URL.

Request API keys Go to SendSignatures