API and webhooks
Create, send, track and download signature requests from your own software, and get notified when they change.
The eSign API lets your own software send PDFs for signature and download the sealed result. eSign calls your software back (a webhook) whenever a request changes.
Address and key
All requests go to your eSign address followed by /api/v1, for example
https://esign.sapiensandmachines.com/api/v1, and carry your workspace's API key:
Authorization: Bearer YOUR_API_KEY
Accept: application/jsonAPI keys belong to the workspace, not to a person, and don't expire. S+M creates them for you; each is shown once. Ask us to revoke a key you no longer use. Up to 120 requests a minute.
Send a document
POST /envelopes with the PDF and the people who sign. Add send=1 to send it straight away. In this example the
key is in the ESIGN_API_KEY environment variable.
curl -X POST https://esign.sapiensandmachines.com/api/v1/envelopes \
-H "Authorization: Bearer $ESIGN_API_KEY" -H "Accept: application/json" \
-F title="Service agreement" \
-F document=@agreement.pdf \
-F "recipients[0][key]=client" \
-F "recipients[0][name]=Ana García" \
-F "recipients[0][email]=ana@example.com" \
-F "recipients[0][role]=signer" \
-F locale=es \
-F send=1| Field | |
|---|---|
title |
Required, up to 255 characters. |
document or document_base64 |
The PDF (up to 25 MB, not password-protected). |
recipients[] |
1 to 20 people: key (your own name for them), name, email, role (signer or cc), routing_order (1–20; the same number signs at the same time), verification (none or email_code for a 6-digit code by email). |
message |
Shown to signers, up to 5,000 characters. |
locale |
en or es; otherwise your workspace's signer language. |
expires_in_days |
1 to 365; 30 if not given. |
external_ref |
Your own reference, to find the request later. |
callback_url |
Where to send this request's webhooks, instead of your workspace's address. |
send |
true to send now; otherwise it stays a draft. |
fields[] |
Optional: place fields by position instead of markers. |
The PDF's markers decide where each signer signs: @2 in a marker means the second signer in recipients.
Other calls
| Call | Does |
|---|---|
GET /envelopes |
Lists requests; filter by status or external_ref; per_page up to 100. |
GET /envelopes/{id} |
One request, with each recipient's status. |
POST /envelopes/{id}/send |
Sends a draft. |
POST /envelopes/{id}/void |
Cancels it (reason); not once it's completed. |
GET /envelopes/{id}/document |
The sealed PDF once completed (409 before that); its SHA-256 is in the X-Document-Sha256 header. |
GET /envelopes/{id}/audit |
The history, and whether its chain of records is intact (chain_intact). |
A request's status is draft, sent, completed, declined, voided or expired. Each recipient's status shows
whether they have viewed or signed.
Webhooks
eSign sends a POST with JSON to the request's callback_url, or else to your workspace's webhook address, for:
| Event | When |
|---|---|
recipient.viewed |
A signer opened the document. |
recipient.signed |
A signer signed. |
envelope.completed |
Everyone signed; the sealed PDF is ready. |
envelope.declined |
A signer declined. |
envelope.expired |
The request expired. |
envelope.voided |
It was cancelled. |
{
"event": "envelope.completed",
"occurred_at": "2026-10-15T15:04:05Z",
"recipient": null,
"envelope": {
"id": "…",
"status": "completed",
"title": "Service agreement",
"external_ref": "QUOTE-1042",
"sealed_sha256": "…",
"completed_at": "2026-10-15T15:04:05Z",
"document_url": "…"
}
}For recipient events, recipient has their id, name, email, status, viewed_at, signed_at and, for a
decline, decline_reason.
Answer with any 2xx status. Otherwise eSign tries again after 10 seconds, 1 minute, 5 minutes, 15 minutes and 1
hour.
Check that a webhook is from eSign
Each webhook carries X-Esign-Event and X-Esign-Signature: sha256=…, an HMAC-SHA256 of the raw body with your
webhook secret. Compute it yourself and compare before trusting the body:
$body = file_get_contents('php://input');
$expected = 'sha256='.hash_hmac('sha256', $body, $webhookSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_ESIGN_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}Errors
Errors come back as JSON with an HTTP status: 401 for a missing or wrong key, 403 when the workspace is suspended,
404 for an unknown request, 409 when asking for the document before it's completed, 422 with the reasons when
something in the request is wrong (for example a marker for a signer who wasn't given, or sending a request that isn't
a draft), and 429 when there are too many requests.