# 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:

```http
Authorization: Bearer YOUR_API_KEY
Accept: application/json
```

API 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.

```bash
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](https://docs.sapiensandmachines.com/en/esign/templates-and-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. |

```json
{
  "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:

```php
$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.
