# Embedded signing

Let recipients sign inside your app or on an eSignStack page, using short-lived signing sessions your server creates through the API.

Your server creates a signing session for one recipient and gets back a single-use URL. You either load it in an iframe (`embedded`) or send the browser to it (`hosted`). eSignStack runs the signing page in both cases, including consent and any required authentication.

## Before you start

- Get an API key with `envelopes:write`. See the [API quick start](/docs/api-authentication).
- In **Settings → Embedding**, add each origin that will frame the signing page, such as `https://app.example.com`. With no origins, embedding is off.
- On the same page, add **Allowed return origins** if you want people sent back to your app afterward. Use one exact origin per line, with no path or wildcard.
- Send the envelope. A session can only be created for a recipient whose turn it is. To keep eSignStack from emailing signing links to anyone, create the envelope with `send_emails: false`. To hold them back for some recipients only, set [`embedded: true`](/docs/envelopes#signing-in-your-app) on those recipients; the others still sign from their email.

## Create a signing session

Call this from your server:

```bash
curl -X POST https://esignstack.app/api/v1/envelopes/$ENVELOPE_ID/recipients/$RECIPIENT_ID/signing_sessions \
  -H "Authorization: Bearer $ESIGNSTACK_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-456-sign-1" \
  -d '{
    "mode": "embedded",
    "parent_origin": "https://app.example.com",
    "return_url": "https://app.example.com/orders/456"
  }'
```

| Field           | Notes                                                                 |
| --------------- | --------------------------------------------------------------------- |
| `mode`          | `embedded` or `hosted`                                                |
| `parent_origin` | Required for `embedded`. Must exactly match an origin in **Settings → Embedding**. |
| `return_url`    | Optional. Must be on an allowed return origin.                        |
| `appearance`    | Optional theme on top of your branding: `primary` (`#rrggbb`), `radius` (`0px` to `24px`), `font` (`instrument-sans`, `system` or `serif`), `dark_mode` |
| `actor`         | Optional `name`, `email`, `external_id` and `auth_method` of the person your app signed in. Recorded in the envelope history. |

The response includes `id`, `url` and `expires_at`. The URL works once and must be opened within 10 minutes. After it opens, the session ends after 60 minutes without activity or 8 hours at most. Create a new session (with a new idempotency key) whenever you need another link. Don't store signing URLs or write them to logs.

The recipient still has to pass any authentication set on them, such as an access code or SMS code.

## Embed the signing page

Set the iframe's `src` to the session URL without changing it:

```js
const frame = document.createElement('iframe');
frame.src = session.url;
frame.title = 'Sign your agreement';
frame.style.width = '100%';
frame.style.height = '800px';
frame.style.border = '0';
document.querySelector('#signing').append(frame);
```

The page sets `frame-ancestors` to your `parent_origin`, so it can only be framed there.

For `hosted`, leave out `parent_origin` and redirect the browser to the URL.

## Listen for messages

The embedded page posts messages to `parent_origin`. Each message has an `event` key:

| `event`                            | When                                                 |
| ---------------------------------- | ---------------------------------------------------- |
| `esignstack:ready`                 | The page loaded                                      |
| `esignstack:page.viewed`           | A page scrolled into view (`pageNumber`)             |
| `esignstack:consent.accepted`      | The recipient agreed to sign electronically          |
| `esignstack:field.completed`       | A field was filled in (`fieldId`, `fieldType`)       |
| `esignstack:recipient.signed`      | The signer finished                                  |
| `esignstack:recipient.approved`    | The approver approved                                |
| `esignstack:recipient.acknowledged`| The recipient confirmed they read the documents      |
| `esignstack:recipient.declined`    | The recipient declined                               |
| `esignstack:session.expired`       | The session expired                                  |
| `esignstack:error`                 | Something went wrong (`error`, for example `envelope_voided`) |
| `esignstack:resize`                | The content height changed (`height`)                |

Completion messages include `session`, `envelope`, `recipient` and `status`. Field values aren't sent unless you turn on **Include field values in embed events** in **Settings → Embedding**.

```js
window.addEventListener('message', (event) => {
    if (event.origin !== 'https://esignstack.app' || event.source !== frame.contentWindow) return;
    if (event.data?.event === 'esignstack:resize') frame.style.height = `${event.data.height}px`;
});
```

## Return to your app

If you set `return_url`, the page goes there when the recipient finishes, declines, or the session ends (for an embedded session, inside the iframe). It adds `session`, `envelope`, `recipient` and `status` (`signed`, `approved`, `acknowledged`, `declined`, `expired` or `cancelled`) as query parameters. Any query string already on `return_url` is replaced.

## Confirm the result on your server

Messages and redirects only update your interface. Before you mark an order signed, confirm with `GET /api/v1/envelopes/{envelope}` or a verified `recipient.signed` or `envelope.completed` [webhook](/docs/webhooks). Get the completed PDF from `GET /api/v1/envelopes/{envelope}/artifacts`.

## Usage

Creating a session for an envelope that was sent in the app counts it as an API envelope, once. See [API envelope limits](/docs/api-authentication#api-envelope-limits).

## Embed a direct link

A [direct link](/docs/share-links) can also be framed. Use `/embed/d/{token}` with the same token as the link's `/d/{token}` URL, on a page whose origin is in **Settings → Embedding**. Optional query parameters:

| Parameter                 | Does                                                    |
| ------------------------- | ------------------------------------------------------- |
| `signerName`, `signerEmail` | Prefill the signer's name and email                   |
| `lockName`, `lockEmail`   | `true` stops the signer changing them                   |
| `externalId`              | Stored as the new envelope's `external_id`              |
| `redirectUrl`             | Where to go afterward. Must be on an allowed return origin. |

## Common errors

| Response                 | Cause                                                          |
| ------------------------ | -------------------------------------------------------------- |
| `422` on `parent_origin` | The origin isn't listed in **Settings → Embedding**            |
| `422` on `return_url`    | The URL isn't on an allowed return origin                      |
| `403`                    | It isn't this recipient's turn, they've finished, or the envelope isn't out for signature |
| `410` when opening       | The URL was already used or is more than 10 minutes old        |

To let your users prepare envelopes in your app as well, see [Editor sessions](/docs/editor-sessions).
