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 keyEvery request is JSON over HTTPS with a bearer key.
| Base URL | https://sendsignatures.com/send/v1 (the unversioned /send prefix is an alias) |
|---|---|
| Auth | Authorization: Bearer <YOUR_API_KEY> on every request. Missing/invalid ⇒ 401. |
| Content type | Content-Type: application/json on POST/PATCH. |
| Idempotency | Send Idempotency-Key: <uuid> on any POST (/batch, /confirm). Re‑sending the same key returns the original response — never a duplicate. |
| Correlation | external_reference (string) + metadata (object), at batch and per‑document level, are echoed back in every response, status call, and webhook. |
| Rate limits | 300 req/min per key. Every response carries X-RateLimit-Limit/-Remaining/-Reset; over the limit ⇒ 429 + Retry-After. |
| Errors | Always { "error": { "code", "message", "details": [] } } with a conventional HTTP status. See Errors. |
/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.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)
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"] } ]The fields on a template. Supply the prefillable ones in /batch → data (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" }
] }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.
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 | |
|---|---|
template | Template name or id. |
mode | blank (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_hours | Link 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" }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"
Fill one or more templates and create drafts. No email is sent. Returns a submission_id + preview_url per document.
| Field | |
|---|---|
sender_name | Name 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, metadata | Your 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_hours | Draft TTL before auto‑cleanup (default 48, max 168). |
dry_run | true = 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": [] } ] }
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.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
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": [ … ], … }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 }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": [] }
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" } ] }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: draft → sent → opened → completed (or declined / voided).
The executed (signed) PDF. 409 until completed.
curl -s https://sendsignatures.com/send/v1/submissions/42/document -H "Authorization: Bearer $KEY" -o signed.pdf
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
submission.completed webhook also carries short‑lived document_url + certificate_url so you rarely need these directly.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 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.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 } ] }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 } ] }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" } ] }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"
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
/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.Register your callback once, then verify the HMAC signature on every delivery.
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 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": "…" }| Event | When |
|---|---|
submission.created | A draft was created. |
submission.viewed | A signer opened the document. |
submission.signed | A signer finished their part (per‑signer, on multi‑signer docs). |
submission.completed | All signers done. Includes documents.document_url + certificate_url. |
submission.declined | A signer declined. |
submission.voided | Voided or expired. |
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 } }
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)
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).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.
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, … }
Every error is the same shape:
{ "error": { "code": "conflict", "message": "Draft already sent; cannot edit", "details": [] } }
| Status | code | Meaning |
|---|---|---|
| 400/422 | invalid_request | Malformed body or missing required input (details may list fields). |
| 401 | unauthorized | Missing/invalid API key. |
| 403 | forbidden | Not allowed (e.g. simulate on a live key). |
| 404 | not_found | Submission/batch/template not found or not owned by your key. |
| 409 | conflict | State conflict — e.g. editing/deleting an already‑sent draft, or fetching a document before completion. |
| 429 | rate_limited | Over 300/min. Honor Retry-After. |
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