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. - 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, setembedded: trueon those recipients; the others still sign from their email.
Create a signing session
Call this from your server:
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:
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.
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. 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.
Embed a direct link
A direct link 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.