API reference · v1
Send documents for signature from your own software.
A small, boring REST API: upload a PDF or Word file, say who signs where, and get told when it's done. JSON in, JSON out, one bearer key.
Authentication
Create a key in Settings → Developers and send it on every request as Authorization: Bearer es_live_…. We show the key once and store only a hash, so keep it in your secrets manager, not in client-side code. Errors always look like { "error": { "code": "…", "message": "…" } } with a matching HTTP status.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/me | Your account, plan and documents used this month. |
GET | /api/v1/documents | List documents. Query: status (comma-separated), limit (1–100), before (ISO date cursor). |
POST | /api/v1/documents | Upload a PDF or Word document (.doc, .docx, .odt, .rtf; converted to PDF), add signers and fields, and send it (multipart). |
GET | /api/v1/documents/{id} | Status, signers and the full audit trail. |
GET | /api/v1/documents/{id}/download | The signed PDF (?file=original for the unsigned one). |
POST | /api/v1/documents/{id}/void | Withdraw a document that's waiting for signatures. Body: { reason }. |
DELETE | /api/v1/documents/{id} | Permanently delete a draft, voided, declined or completed document. |
GET | /api/v1/templates | Your saved templates and their roles, in the order the send call expects signers. |
POST | /api/v1/templates/{id}/send | Send a saved template. Body: { title?, signers: [{ name, email }] }, one per role in order. |
OpenAPI spec
The whole API is described in one OpenAPI 3.1 file: esignsimple.com/openapi.json. Import it into Postman, Insomnia or Bruno to try the calls, or feed it to a generator such as openapi-generator to get a typed client. It covers every endpoint on this page plus the public GET /api/verify?sha256= check, which needs no key.
Create and send a document
Coordinates are fractions of the page, measured from the top-left corner, so they work at any page size. kind is one of signature, initials, date, name, email, text or checkbox; add required: false or a label where it helps. Set "sequential": true to have signers go in order, or "send": false to keep it as a draft you finish in the editor.
curl https://esignsimple.com/api/v1/documents \
-H "Authorization: Bearer $ESIGN_KEY" \
-F file=@contract.pdf \
-F 'data={
"title": "Consulting agreement",
"message": "Please sign by Friday",
"signers": [{ "name": "Jane Doe", "email": "jane@example.com" }],
"fields": [
{ "signer": 0, "kind": "signature", "page": 0, "x": 0.1, "y": 0.8, "w": 0.3, "h": 0.06 },
{ "signer": 0, "kind": "date", "page": 0, "x": 0.5, "y": 0.8, "w": 0.2, "h": 0.03 }
]
}'The response is the document object with status sent. If your account has no email delivery configured, signing_links contains each signer's personal link so you can deliver it yourself.
Webhooks
Add one HTTPS endpoint in Settings. We send document.completed, document.declined, document.voided and signer.signed as JSON: { id, type, created_at, data }. Each request carries esign-signature, the hex HMAC-SHA256 of the raw body using your endpoint secret. Verify it before trusting the payload, and treat events as at-least-once: we retry a failed delivery once after a few seconds.
import crypto from "node:crypto";
// raw = the exact request body string; header = req.headers["esign-signature"]
const expected = crypto.createHmac("sha256", process.env.ESIGN_WEBHOOK_SECRET).update(raw).digest("hex");
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));What does a document object look like?
Every endpoint that returns a document uses the same shape: id, title, status, status_reason, sequential, pages (the page count), created_at, sent_at, completed_at, original_sha256, final_sha256, download_url and a signers array. Each signer has an id, name, email, order, status and signed_at. download_url and final_sha256 are only filled in once the document is completed.
status is one of draft, sent, finalizing, completed, voided or declined. finalizing is the short window after the last signature while we stamp the fields and add the certificate page; treat it like sent and wait for completed. GET /api/v1/documents/{id} adds an events array, the audit trail: each entry has a type, the time (at), the signer_id where one applies, and the ip.
Store final_sha256 next to the PDF you download. It is the same fingerprint our public verify page checks, so you can later prove the file in your system is exactly the one that was completed.
Listing and paging through documents
GET /api/v1/documents returns your newest documents first, 20 at a time by default and up to 100 with limit. Filter with status, for example ?status=sent,finalizing for everything still out for signature. The response is { data, has_more, next_before }: when has_more is true, pass next_before as before to get the next page. That cursor is a creation date, so new documents arriving while you page never shift your results.
More field types
Besides the kinds above, fields[].kind accepts company, title, number, dropdown and radio. Dropdown and radio fields need options, a list of 2 to 20 choices. signer is the index of the person in your signers array, starting at 0, and page is also zero-based. A practical way to find coordinates: place the fields once in the editor, save the document as a template, and send it with POST /api/v1/templates/{id}/send; then you only pass names and email addresses, one per template role, in order.
Errors and how to handle them
Every error has an HTTP status and a stable code. 401 unauthorized: the key is missing, revoked, or the account is no longer on a plan that includes the API. 400 invalid_request: something in the request is wrong, such as no file, data that is not valid JSON, an unsupported file type, a signer without a valid email address, or the wrong number of signers for a template; the message says which. 404 not_found: the document or template does not exist in your account. 409 not_completed: you asked for the signed PDF before the last signature. 413 too_large: the file is over 25 MB. 422 conversion_failed: a Word file could not be converted to PDF. 500 internal: our fault; retry later and tell us if it persists.
Only retry 5xx responses and network errors, with a growing delay between attempts. Retrying a POST /api/v1/documents after a timeout can create a second document, so check the document list for a recent document with the same title before sending again.
What is in each webhook?
One HTTPS endpoint per account; the secret (it starts with whsec_) is shown when you add it. The data object depends on the event: signer.signed carries document_id and the signer (id, name, email); document.completed carries document_id, title, sha256 of the final PDF and completed_at; document.declined carries document_id, title, the signer (name, email) and their reason; document.voided carries document_id, title and the reason.
We wait up to 10 seconds for your response and retry once if it is not a 2xx. So respond quickly, do the real work in a background job, and use the event id to ignore duplicates. Because there is only one retry, run a periodic catch-up as well: list documents with status=sent,finalizing and check any that are older than you expect.
A typical integration, end to end
Say your CRM should send a consulting agreement whenever a deal is marked as won. Build the agreement once in the editor with two roles, Client and Consultant, and save it as a template. When the deal closes, your server calls POST /api/v1/templates/{id}/send with the client and consultant names and emails, and stores the returned document id on the deal. When document.completed arrives, verify the esign-signature header, call GET /api/v1/documents/{id}/download, attach the PDF to the deal and save sha256 next to it. If document.declined arrives instead, put the reason on the deal and alert the account owner. That is three API calls and two webhook handlers, and it covers the whole lifecycle.
Limits and what's not here yet
API documents count toward your plan exactly like documents sent from the dashboard. Files (PDF or Word) are limited to 25 MB and 10 signers; Word files are converted to PDF on upload, and field coordinates refer to the converted PDF. There's no embedded signing iframe, SDK or sandbox yet; they're on the roadmap, and we'd rather tell you that than pretend. Our take: most integrations need three calls (create, poll or webhook, download), and we'd rather keep those three excellent than ship thirty mediocre ones.
Frequently asked questions
Which plans include the API?
Silver and Gold. There is no separate API price: Silver ($9.99/month) includes the REST API and webhooks with unlimited documents. If an account drops below Silver, its API keys stop working until it upgrades again.
How do I authenticate?
Create a key in Settings → Developers and send it as `Authorization: Bearer es_live_…`. Keys are shown once and stored hashed; revoke one any time and requests using it fail immediately.
How do I know when a document is signed?
Add a webhook URL in Settings. We POST document.completed, document.declined, document.voided and signer.signed events, signed with HMAC-SHA256 in the esign-signature header. You can also poll GET /api/v1/documents/{id}.
Can signers sign inside my app?
Not yet. Embedded signing (an iframe you host) is on our roadmap. Today each signer gets a personal link by email, or you receive the links in the API response to deliver yourself.
Is there a sandbox?
Not yet. Test with "send": false, which creates a draft without emailing anyone (drafts never count toward a plan), and send real test documents to your own addresses. Void and delete them when you are done.
Is there a rate limit or an SDK?
There is no SDK; the API is plain HTTPS and JSON, so any HTTP client works, and the OpenAPI 3.1 file at /openapi.json lets you generate a typed client in most languages. We don't publish a rate limit, but please be reasonable: use webhooks instead of polling every few seconds, and page through the document list with limit and before.
Can I use the API from a browser or mobile app?
Not with an API key. Keys carry full access to your account, so keep them on your server and call the API from there. Your front end talks to your server; your server talks to us.