# Envelopes API

Upload files, build a draft envelope, add recipients and fields, preview it, send it, and correct it after sending.

An envelope holds one or more documents and the people who act on them. Its `status` is `draft`, `sent`, `completed`, `voided`, `declined` or `expired`. All paths below start with `/api/v1`. To start from a template instead, see the [API quick start](/docs/api-authentication#create-an-envelope-from-a-template). Templates themselves can be created, changed and deleted through `/templates`; see the [API reference](/docs/api).

## Upload a file

`POST /files` (ability `files:write`) takes a multipart `file` (a PDF; an Office file: `doc`, `docx`, `odt`, `rtf`, `txt`, `xls`, `xlsx`, `ods`, `ppt`, `pptx`, `odp`; or an image: `jpg`, `jpeg`, `png`, `gif`, `webp`, `heic`, `heif`) or a public HTTPS `url` to a PDF. Office files and images are converted to PDF; each image becomes one Letter page. Optional: `password` for a protected PDF, `use_text_tags` (default `true`) to turn [text tags](/docs/text-tags) into fields, and `import_form_fields: false` to use a fillable PDF as a plain document. Otherwise a fillable PDF's own form fields are found and set up; see [Fillable PDF forms](#fillable-pdf-forms). An AI agent with a file on its own computer uses the MCP tool `create_upload_link` instead of a public URL; see [Uploading a local file](/docs/mcp#uploading-a-local-file).

```bash
curl -X POST https://esignstack.app/api/v1/files \
  -H "Authorization: Bearer $ESIGNSTACK_KEY" \
  -H "Idempotency-Key: order-456-file" \
  -F file=@agreement.pdf
```

The file's `status` is `processing`, then `ready` or `failed`. Check it with `GET /files/{file}` or wait for the `file.ready` or `file.failed` event.

## Create a draft

`POST /envelopes` creates a draft from a ready file:

```json
{
  "file": "file_01J…",
  "name": "Acme service agreement",
  "external_id": "order-456",
  "email_subject": "Please sign the Acme agreement",
  "expires_in_days": 30
}
```

Add `folder` (an envelope folder ID, `fdr_…`) to put the draft in a folder. A file that isn't ready yet returns `409 resource_conflict`. Change draft settings with `PATCH /envelopes/{envelope}` and delete a draft with `DELETE /envelopes/{envelope}`; on an envelope that's already sent, both return `409 resource_conflict`. `send_emails: false` means eSignStack sends no emails for the envelope; your app brings people to sign with [signing sessions](/docs/embed-integration).

## Documents and pages

`PUT /envelopes/{envelope}/documents` replaces the draft's documents. Each document is built from pages of uploaded files (`file`) or of documents already in the envelope (`document`), with optional `rotation` (0, 90, 180 or 270):

```json
{
  "documents": [
    { "name": "Agreement", "pages": [{ "file": "file_01J…", "page": 1 }, { "file": "file_01J…", "page": 2 }] },
    { "name": "Pricing", "pages": [{ "file": "file_01J…", "page": 1 }], "visible_to": ["rcp_01J…"] }
  ]
}
```

Fields move with their page. Fields on pages you drop are removed. `GET /envelopes/{envelope}/pages` lists every page in the order fields use.

`visible_to` limits who sees a document. It's a list of recipient IDs: `null` means everyone and `[]` means no one. A recipient's signing copy contains only the documents they can see, and you can't give a recipient a field on a document they can't see. You can also set a recipient's `documents` (a list of `doc_…` IDs) from the recipient endpoints.

## Recipients

Add, change and remove draft recipients with `POST /envelopes/{envelope}/recipients`, `PATCH …/recipients/{recipient}` and `DELETE …/recipients/{recipient}`.

| `type`     | Shown in the app as  | What they do                                              |
| ---------- | -------------------- | --------------------------------------------------------- |
| `signer`   | Signer               | Completes their fields and signs                          |
| `approver` | Approver             | Reviews the documents and clicks **Approve** or **Reject** |
| `viewer`   | Needs to acknowledge | Reviews the documents and confirms they've read them      |
| `cc`       | Gets a copy          | Gets the completed documents; takes no action             |

An approval or acknowledgment is recorded in the envelope history and on the certificate. Nothing is added to the PDF. Only signers can have fields.

- `routing_order` (1 to 99): recipients with the same number act at the same time; higher numbers wait for lower ones.
- `role`: a label such as `Client`.
- `auth_method`: `none`, `access_code` (with `auth_access_code`), `sms` (with `auth_phone`) or `email_otp`. Any method other than `none` needs the Platform plan.
- `receive_completed_pdf`: whether the recipient is emailed the completed documents.
- `allow_delegation`: see [Reassigning](#reassigning).
- `embedded`: see [Signing in your app](#signing-in-your-app).

## Signing in your app

Set `embedded: true` on a recipient who signs inside your app, for example someone sitting with you, while others on the same envelope sign from their email. It works like DocuSign's `clientUserId`. eSignStack sends that recipient no invitation, reminder, next-signer or "document updated" email, and the app shows no resend or remind button for them. You open their signing page with a [signing session](/docs/embed-integration), hosted or embedded.

Everything else stays the same. `recipient.activated` fires when it's their turn, so in a signing order you know when to open their session. Other webhooks fire as usual. Their completed copy and void or decline notices follow your [notification settings](/docs/notifications), and email or SMS verification codes still go out, since the signer asks for them.

- An embedded recipient still needs an email address, like any signer.
- It defaults to `false`. Set it when you add the recipient, on a draft, or with a [correction](#corrections) before that recipient acts. Leaving it out of an update keeps the current value.
- `cc` and `in_person_signer` recipients can't be embedded (`422`).
- In `POST /templates/{template}/envelopes`, set `embedded` on an entry in `recipients`.

## Fields

`POST /envelopes/{envelope}/fields` adds a field; `PATCH` and `DELETE` on `…/fields/{field}` change or remove one.

```json
{
  "type": "signature",
  "recipient": "rcp_01J…",
  "document": "doc_01J…",
  "page": 1,
  "x": 72, "y": 640, "width": 180, "height": 40,
  "required": true
}
```

Positions are in PDF points from the top-left corner of the page. `page` is counted within `document`; a negative number counts from the end. `type` is one of `signature`, `initials`, `date_signed`, `name`, `email`, `company`, `title`, `text`, `number`, `date`, `checkbox`, `checkbox_group`, `radio_group`, `dropdown`, `attachment` or `repeat_group`. Every field needs a `recipient`, unless you set `prefill: true` and fill it yourself with `value` before sending. Other options: `label`, `options` (for choice fields), `validation` and `tab_order`. See [Fields](/docs/fields) for what each type does.

## Fillable PDF forms

An envelope made from a fillable PDF gets the form's own fields. Fields with a value are kept as filled; empty ones go to the first signer once you add one. To choose which fields recipients fill, list them and assign the ones you need. The rest stay as they are in the PDF.

`GET /envelopes/{envelope}/form_fields` (ability `envelopes:read`) lists one entry per form field: `name`, `label`, `type`, `page` and `box`, `value`, `required`, `read_only`, and `status` (`assigned`, `kept` or `unassigned`) with its `recipient`. A field with several boxes, such as a radio button per choice, lists each in `widgets`. The response also has an `acceptance_digest`.

`PUT /envelopes/{envelope}/form_fields` (ability `envelopes:write`) replaces the choice:

```json
{
  "acceptance_digest": "9f2c…",
  "fields": [
    { "name": "FullName", "recipient": "rcp_01J…", "required": true },
    { "name": "Email", "recipient": "rcp_01J…", "label": "Signer email" }
  ]
}
```

- Every form field you leave out keeps its current value, and nobody fills it.
- Read-only form fields always stay as they are.
- Signature and initials fields can't stay empty. One you leave out keeps its recipient; if it has none, the request is refused.
- If the form changed since you listed it, the request is refused with `acceptance_digest` as the `param`. List it again.

When the list has `requires_acceptance: true`, the upload was digitally signed or was a converted XFA form, and `notices` explain what changed. Sending needs the report accepted, and the `PUT` request accepts it for your key.

Templates work the same way with `GET` and `PUT /templates/{template}/form_fields` (abilities `templates:read` and `templates:write`), using `role` instead of `recipient`.

## Preview

`POST /envelopes/{envelope}/preview` (ability `envelopes:write`) renders the draft as it is now, watermarked "DRAFT — not for signature". It returns a `url` that works for 15 minutes. If the draft changes, request a new preview.

To show the preview inside your app, send `parent_origin`. It must exactly match one of the origins in **Settings → Embedding**. The response adds `preview_session`; put `preview_session.url` in an iframe. Open it within 10 minutes; the read-only session then lasts 15 minutes. The page posts `esignstack:preview.ready`, `esignstack:preview.stale` (the draft changed), `esignstack:session.expired`, `esignstack:error` and `esignstack:resize` to your page. It has no signing or editing actions.

## Send

`POST /envelopes/{envelope}/send` (ability `envelopes:send`) sends the draft. A live send counts toward your plan's [API envelope limit](/docs/api-authentication#api-envelope-limits).

After sending:

| Endpoint                          | Does                                                    |
| --------------------------------- | ------------------------------------------------------- |
| `POST …/remind`                   | Emails a reminder. Optional `recipient` for one person. |
| `POST …/extend`                   | Adds `days` (1 to 365) to the expiry. `notify_pending_signers` is optional. |
| `POST …/void`                     | Voids the envelope. `reason` is optional (up to 1000 characters; a generic one is used without it). It goes in the audit log and in the "voided" email to everyone who was invited. |
| `POST …/duplicate`                | Copies the envelope into a new draft.                   |
| `POST …/move`                     | Moves the envelope to `folder`, or out of its folder with `null`. Works for any status. |
| `GET …/artifacts`                 | Links to the completed PDFs, valid for 5 minutes (ability `artifacts:read`). |
| `GET …/audit_trail`               | The audit trail as data: the same entries as the certificate (event, actor, `ip_address`, `occurred`), in any status. |
| `POST …/archive`, `POST …/restore` | Archives or restores an envelope (`archived`). Void an envelope out for signature first; a legal hold blocks archiving (`409`). |
| `POST …/legal_hold`               | Places a legal hold (`legal_hold`). Only an owner or admin can release it, in the app. |
| `POST /envelope_downloads`        | One ZIP with the signed PDFs of up to 50 completed `envelopes`, behind a 10-minute link (ability `artifacts:read`). |

Archived envelopes are left out of `GET /envelopes`; `GET /envelopes?filter[archived]=true` lists only them. They can still be retrieved, with their audit trail. A deleted draft is archived too, and an archived draft is purged after the workspace's deleted-draft retention period. Filter lists with `filter[status]`, `filter[external_id]`, `filter[template]`, `filter[created][gte]`/`filter[created][lte]` and `filter[metadata][key]=value`.
| `POST …/retry-finalization`       | Retries a completion that failed (`envelope.finalization_failed`). |

## Corrections

`POST /envelopes/{envelope}/corrections` (ability `envelopes:send`) changes an envelope that's out for signature:

- `recipients`: change the name, email or authentication of recipients who haven't acted, by `id`. A new email, phone or authentication method stops their old link, and they get a new one if it's their turn. A name change keeps the link.
- `add_recipients`: add recipients to routing stages that haven't started.
- `remove_recipients`: remove recipients who haven't acted.
- `documents`: a replacement document list, in the same shape as the documents endpoint.
- `fields`: a complete replacement field list.
- `note` and `expected_revision`, which rejects the change if the envelope changed since you read it.

A correction can turn `embedded` on or off for a recipient who hasn't acted. Either way their current link stops working: turned on, they sign only through signing sessions; turned off, eSignStack emails them a new link.

Send `documents` and `fields` in separate requests: correct documents first, then fields using the new `doc_…` IDs. Documents and fields can change only until any recipient has acted. After that the API returns `409 correction_requires_void`; void the envelope and send a new one. Content changes keep signing links and signing sessions working: they open the new version, and an action from a page that still shows the old one is refused, so the page reloads with the new version. Everyone consents again, and everyone whose turn it is gets an email with their existing link. Earlier versions stay in the envelope history.

## Reassigning

Set `allow_delegation: true` on a signer, approver or recipient who needs to acknowledge to let them pass their invitation to someone else before acting. It defaults to `false`, and leaving it out of an update keeps the current setting. Recipients who get a copy can't delegate.

Template roles have the same setting. Envelopes made from a template inherit it, and an explicit `allow_delegation` on a recipient in `POST /templates/{template}/envelopes` overrides it.

The recipient reassigns from the signing page (Reassign to someone else). Their link stops working, the new person gets an invitation, and the change is recorded in the envelope history. The new person still has to pass any required authentication.

## Test mode

Test keys use every endpoint on this page, including upload, create, send, correct and duplicate, on test data that's kept separate from live data. Test envelopes don't count toward any limit, every page is marked as a test, and emails go only to verified members of the workspace. Sending with SMS verification returns `422`. See [Sandbox](/docs/sandbox).
