06 · EMBEDDING

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.

bash
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" }'
Body (optional)
FieldTypeNotes
expires_inintegerSeconds, 60 to 3600. Defaults to 900.
redirect_urlstringWhere to send the signer when they finish. Must start with https://.
200 OK
{
  "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.

Reassigning a signer invalidates their links
If a recipient forwards or is reassigned, their token rotates and every outstanding embed link for the person they replaced stops working. Mint a fresh one rather than reusing.

Render it

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

Events
FieldTypeNotes
esign:readydocumentIdThe exchange succeeded and the document is displayed.
esign:signeddocumentIdThis recipient submitted their signature.
esign:completedsignedPdfUrl, auditPdfUrlEveryone has signed and the PDF is sealed. Either URL may be null.
esign:declined-The signer declined.
esign:errormessage, codeThe exchange failed - usually an expired link or an origin that is not on the allowlist.
javascript
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;
  }
});
The browser message is a hint, not the truth
It comes from a page in the user's browser, so treat it as a cue to update your interface and nothing more. The webhook is what your database should believe, because it arrives server to server and is signed. Fetch the signed PDF with 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-ancestors refused the frame. The browser console will say so plainly.