SignEnvoy docs
Sign in Start in the sandbox

Guides

Embedding the signing page

The ceremony is a white-labeled signing page built to be embedded. Your server mints an embed session for one recipient, your page or app opens the returned URL in an iframe or a WebView, and the ceremony reports ready, completed, rejected and error on the channel you chose when you minted it. Everything the recipient sees and signs is the ordinary ceremony: consent, verification, the certificate.

1. Allow your origins

In the dashboard, Settings → Workspace → Embedding, list the origins whose pages may frame your signing page: a scheme, a host and an optional port, like https://app.example.com. One per line. No wildcards, no paths. http://localhost:5173 is accepted for development. Browsers enforce this list through the signing page's frame-ancestors policy, so a page on an unlisted origin never loads the ceremony at all. The list is empty by default, which means embedding is off.

2. Mint a session on every render

Server side, with your API key, once the envelope is sent. The session works once, for ten minutes. Mint it in the request that renders the page; never store or cache the URL.

Pages render twice more often than you think (a development strict mode, a retry after a dropped response). Send an Idempotency-Key on the call that creates the envelope, keyed on your own record (an order id), so a second render cannot make a second document; the API honours the key for 24 hours. Minting a second session is harmless.

curl -s -X POST https://app-staging.signenvoy.com/v1/documents/doc_…/recipients/{recipient_id}/embed-session \
  -H "Authorization: Bearer $SENV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_delivery": "message"}'
{
  "recipient_id": "…",
  "embed_url": "https://app-staging.signenvoy.com/s/…?event_delivery=message",
  "expires_at": "2026-09-21T16:54:52Z",
  "event_delivery": "message"
}

The body is optional; event_delivery defaults to message. Section 4 explains the three values. The URL carries the value you chose, and the ceremony reads it from there; it is not a secret and nothing is stored.

409 invalid-state unless the envelope is in flight and the recipient has not signed or declined; 404 not-found for a recipient outside the envelope. A test-mode key mints sessions for test-mode envelopes only.

Step 1 is checked at the mint. A message session from a workspace with an empty origins list can be shown by no page of yours, so a live-mode key gets 409 embed-origins-missing and nothing is minted, and a test-mode key gets the session plus a warnings entry with that code (and a Warning header) so you learn on the first call rather than from a blank frame. Both carry a settings_url for the Embedding page. Only an empty list is caught: a wrong entry still fails in the browser, and the dashboard shows a notice while the list is empty and a framed session was minted in the last seven days. redirect and none sessions need no origin and are never refused.

Opening the URL issues that recipient's signing link and loads the ceremony in place. This rotates the link, exactly like the signing-link mint: a link SignEnvoy emailed earlier stops working. When you embed, send with "delivery": "none" so there is no emailed link to invalidate. With "none", recipients must use auth_level: "link": a one-time code needs SignEnvoy to deliver the link, and the send refuses the combination (422).

The envelope's timeline records the issue as link_reissued with channel: "embed", and the certificate says the link the signer used was "delivered by the sender".

3. Put it in your page

With the React component:

import { SignEnvoyCeremony } from "@signenvoy/embed-react";

<SignEnvoyCeremony
  embedUrl={session.embed_url}
  onCompleted={(envelopeId) => markSigned(envelopeId)}
  onRejected={() => showDeclined()}
  onError={(slug) => slug === "embed-session-expired" && refresh()}
/>

Without React:

import { mountCeremony } from "@signenvoy/embed-react/core";

const mounted = mountCeremony(envelope.querySelector("#sign"), {
  embedUrl: session.embed_url,
  onEvent: (e) => console.log(e.event, e),
});
// later: mounted.unmount()

Or an iframe by hand. The ceremony posts messages to your page's origin only, shaped { source: "signenvoy-ceremony", event, ... }; check event.origin against the API host and event.source against the frame before you trust one. The package does both.

4. How events reach you

event_delivery on the mint names the channel, because your server knows what will open the URL and the ceremony should not guess.

valueuse it forwhat the ceremony does
message (default)an iframe in your web page; a WebView with an injected script In a frame, posts to your page's origin as above. Loaded top-level (a WebView), posts the same payload to its own window, where a script you injected can listen and forward to native code.
redirecta WebView, when you would rather intercept a URL than inject a script On completed, rejected or error only, navigates to signenvoy-message://<event>?…. Your navigation handler cancels it and reads it. Nothing reaches the network; a desktop browser ignores it. Progress events are not sent.
noneyou rely on webhooks aloneNothing.

The payload is the same on every channel: the event name plus envelopeId on ready and completed, role on completed, slug on error. envelopeId is the document's doc_… id, the one GET /v1/documents/{id} and the webhooks use. Whichever channel you use, webhooks remain the record. Anything that arrives through the signer's own device is a cue for your interface, not proof.

iOS (WKWebView)

// mint with event_delivery: "redirect", then load embed_url top-level
func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  if let url = action.request.url, url.scheme == "signenvoy-message" {
    let event = url.host ?? ""                      // "completed" | "rejected" | "error"
    let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
    handleCeremony(event, items)                    // e.g. pop the signing screen
    decisionHandler(.cancel)
    return
  }
  decisionHandler(.allow)
}

Android (WebView)

override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
  val url = request.url
  if (url.scheme == "signenvoy-message") {
    handleCeremony(url.host ?: "", url.getQueryParameter("envelopeId"), url.getQueryParameter("slug"))
    return true                                      // cancel the navigation
  }
  return false
}

React Native (react-native-webview)

import { parseCeremonyRedirect } from "@signenvoy/embed-react/core";

<WebView
  source={{ uri: session.embed_url }}
  onShouldStartLoadWithRequest={(req) => {
    const event = parseCeremonyRedirect(req.url);
    if (!event) return true;                         // an ordinary navigation
    if (event.event === "completed") markSigned(event.envelopeId);
    if (event.event === "error" && event.slug === "embed-session-expired") refresh();
    return false;                                    // cancel it
  }}
/>

Prefer message in a WebView? Inject a script at document start that listens on window, accepts a message only when its origin is the API host and its source is the window itself (isCeremonySelfMessage in the package does this check), and forwards it through your platform's bridge.

A frame on an origin that is listed but could not be identified (for example, two or more listed origins and a host page that suppresses its referrer) posts nothing and writes one warning to the browser console naming the framing origin. List exactly the origins that embed, and check the console before the webhooks.

5. The events

eventwhencarries
readythe ceremony loaded and the recipient can actenvelopeId
consentedthe recipient accepted electronic signing
field_completedone field was completedfieldId
completedthe recipient finished: signed, approved when role is approver, or marked as viewed when it is viewerenvelopeId, role
rejectedthe recipient declined
errorthe ceremony could not proceedslug, from the error reference
redirect_requestedthe envelope's redirect_url is about to be followedurl

completed means the recipient signed or, for an approver (role: "approver"), approved: an approval is not a signature. For a viewer (role: "viewer") it means they marked the document as viewed, which is neither. The sealed PDF follows a moment later. Use the document.sealed webhook, or poll the envelope, before you offer a download. The events are a convenience for your page; webhooks remain the record.

If the envelope has a redirect_url, the ceremony ends with a destination confirmation ("the sender asks to send you to host") and moves only when the signer clicks, posting redirect_requested first; nothing navigates on its own, and an embedded frame never navigates its parent. The URL must be https://; an http:// value is refused when the envelope is created. An application on plain localhost should omit it, or use a tunnel's https origin, while developing.

6. When the frame says "session ended"

A reused URL, a URL older than ten minutes, or an envelope that changed state between the mint and the open renders the recipient a plain "this signing session has ended" page and reports error with slug: "embed-session-expired" on your channel. Mint a fresh session and render again. See embed-session-expired.

A page on an origin you have not listed gets nothing: the browser refuses to show the frame, and no event is posted. The attempt still spends the session (the browser applies the frame policy to the response it received), so after fixing the origins list, mint again. Check the list first when a frame stays blank.

7. Abandoned signings

On a "delivery": "email" envelope, the automatic reminder never replaces a link an embed session issued: you have taken over delivery for that signer. On a "none" envelope SignEnvoy emails nobody unless you ask. To recover a signer who left your page without finishing, set reminder_cadence_days on the envelope or call send again: SignEnvoy then emails the signer a fresh link, which replaces the embedded one and is recorded with channel: "email". See Signing links.