Embedded signing
Let people sign inside your own app instead of emailing them a link. Covers the exchange, browser events, and the origin allowlist.
All documentation
Embedded signing keeps the signer inside your product. Instead of emailing a link and hoping they come back, you drop the signing page into an iframe and they finish without a context switch. It is the difference between a checkout that completes and one that resumes tomorrow.
Allow your origin first
In the web app, under Settings, Developer, Embedding, add every origin that will frame the signing page. An empty list means embedding is off, and minting a link returns 409 embedding_not_configured.
Origins are stored as scheme, host and port with no path. https:// is accepted anywhere. http:// is accepted only on localhost, 127.0.0.1 and [::1], so you can develop locally without a tunnel, and nowhere else.
Mint a link on your server
The document must be in progress and the recipient must be a signer who has not already finished.
curl https://api.documentesign.com/v1/documents/doc_7a1b.../recipients/rcp_3d9e.../embed-link \
-H "Authorization: Bearer $ESIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "expires_in": 900, "redirect_url": "https://app.example.com/done" }'| Field | Type | Notes |
|---|---|---|
expires_in | integer | Seconds, 60 to 3600. Defaults to 900. |
redirect_url | string | Where to send the signer when they finish. Must start with https://. |
{
"url": "https://app.documentesign.com/sign/embed/eyJhbGciOi...",
"expires_at": "2026-09-01T10:15:00.000Z"
}What that URL actually holds
Not the recipient's signing token. It carries a short-lived signed envelope, which the page exchanges once, server side, for the real token. The token is held in memory and never reaches the address bar, so the URL cannot be lifted from a browser history or a server log and reused later.
Because it is short-lived, mint it at the moment you are about to render the iframe rather than storing it against a record.
Render it
<iframe
src={embedUrl}
title="Sign the document"
style={{ width: "100%", height: 720, border: 0 }}
/>Listen for what happens
The frame posts messages to the parent window. Every message carries source: "document-esign". Check the origin before you trust one, or any page could fake a completion and walk your UI forward.
| Field | Type | Notes |
|---|---|---|
esign:ready | documentId | The exchange succeeded and the document is displayed. |
esign:signed | documentId | This recipient submitted their signature. |
esign:completed | signedPdfUrl, auditPdfUrl | Everyone has signed and the PDF is sealed. Either URL may be null. |
esign:declined | - | The signer declined. |
esign:error | message, code | The exchange failed - usually an expired link or an origin that is not on the allowlist. |
const signerOrigin = new URL(embedUrl).origin;
window.addEventListener("message", (e) => {
if (e.origin !== signerOrigin) return;
if (e.data?.source !== "document-esign") return;
switch (e.data.type) {
case "esign:completed":
// A hint for your interface. Confirm server-side before you act on it.
refreshFromServer(documentId);
break;
case "esign:declined":
showDeclined();
break;
case "esign:error":
showError(e.data.message);
break;
}
});GET /documents/{id}/files/signed.Two shortcuts worth knowing
If you are sending from a template and the signer is about to sign right away, POST /v1/templates/{id}/send accepts an embed object and returns the links with the document, so there is no second call. See send from a template.
If you do not know who is signing - a form on your website, the same agreement for anyone who asks - embed a template link instead. The visitor supplies their own name and email, and each visit becomes its own document.
Why frame-ancestors is the control that matters
The signing page reports its own framing origin during the exchange, which produces a clear error when you have forgotten to allowlist a domain. It is not a security boundary - a hostile embedder could claim anything.
The real enforcement is the frame-ancestors content security policy served with the signing page, built from your workspace allowlist and applied by the browser. A domain that is not on your list cannot frame the page at all, whatever it claims to be.
When the iframe stays blank
- 409 embedding_not_configured - the workspace has no allowed origins yet.
- 409 document_not_in_progress - the document is still a draft, or already finished.
- 401 on exchange - the link expired. They are deliberately short-lived; mint a new one.
- Blocked by the browser - your origin is missing from the allowlist, so
frame-ancestorsrefused the frame. The browser console will say so plainly.