API and webhooks
Create, send, track and download signature requests from your own software, and get notified when they change.
The dVellum API lets your own software send PDFs for signature and download the sealed result. dVellum calls your software back (a webhook) whenever a request changes.
Address and key
All requests go to your dVellum address followed by /api/v1, for example
https://dvellum.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. Admins create them in Settings → API keys; each is shown once. Revoke a key there when you no longer use it. 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 DVELLUM_API_KEY environment variable.
curl -X POST https://dvellum.sapiensandmachines.com/api/v1/envelopes \
-H "Authorization: Bearer $DVELLUM_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; your workspace's default (30) if not given. |
remind_every_days |
1 to 30: email signers who haven't signed every so many days; null for none. Your workspace's default 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. |
sender |
{"name": …, "email": …}: the person you send for. Replies to signer emails reach them, and they are emailed when it is viewed, signed, declined, completed (with the signed PDF) or expires. Without it: your workspace's reply 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
dVellum 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 dVellum tries again after 10 seconds, 1 minute, 5 minutes, 15 minutes and 1
hour.
Check that a webhook is from dVellum
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.