Embedded signing
Your customers sign directly in your application: you show our signing page in an iframe on your own website. This works at every level where external recipients act (SES, AES), and for approvers and viewers. Qualified signatures (QES) are never embedded: members with a certificate sign in the dashboard or through the approval link.
This guide is the English version of the German guide embedded-signing and covers the same steps.
Your business logic still relies on the webhook (or GET /v1/events). The messages of the embedded page are hints for your user interface, not a confirmation.
1. Set the allowed websites
Only websites on the list of your organization may embed the signing page. The list is empty until you fill it, and empty means embedding is off.
PUT /v1/embed-settings
Authorization: Bearer qgw_live_…
Idempotency-Key: 5d1f…
Content-Type: application/json
{ "allowedOrigins": ["https://app.example.com", "https://*.example.org"] }
Response:
{ "object": "embed_settings", "allowedOrigins": ["https://app.example.com", "https://*.example.org"] }
Rules for every entry:
https://host[:port]without path, query or credentials, orhttps://*.hostfor all subdomains of a domain you own (not*.vercel.app, not*.co.ukand similar public suffixes).- No bare
*, no IP addresses, nohttp://, excepthttp://localhost:<port>with a test key; such entries apply in test mode only. - At most 20 entries, each only once. A rejected entry answers 422 / 9012 with
details.reason(origin_invalid,origin_duplicate) anddetails.index. - Scope
org:manageto change (off by default),org:readto read (GET /v1/embed-settings). In the dashboard, owners and administrators change the list. - Every change appears in the activity log (
EMBED_SETTINGS_UPDATED).
Changes apply immediately to our signing page and to new embedded sessions. For the signing step of SES/AES, the signature service receives the list when the signing round starts; a later change only affects new rounds. There, only exact origins count (entries with * allow only our page, not the signing step), and at most nine of your origins per round.
The same rule applies per mode to the signing page, embedded sessions and the signing step: in live mode only the https entries count, and http://localhost:<port> and other test-only entries count in test mode only. If embedding is switched off for your organization (tier feature) or still blocked in live mode, the signing step receives none of your origins either. Qualified signatures (QES) are never embedded.
2. Request an embedded session
As soon as a recipient is up next (the event recipient.ready), your server requests a session for the website on which you show the page:
POST /v1/envelopes/{id}/recipients/{rid}/embedded-session
Authorization: Bearer qgw_test_…
Idempotency-Key: 8c2e…
Content-Type: application/json
{ "origin": "https://app.example.com", "locale": "en" }
{
"object": "embedded_session",
"url": "https://sign.<domain>/s/<token>?embed=1",
"expiresAt": "2026-09-30T12:05:00Z",
"origin": "https://app.example.com"
}
- Scope
envelopes:signing_urls(off by default), rate limitwrite. - The URL is valid for 5 minutes and exactly once, and only embedded on this origin. On the first load the page exchanges it for a session that it keeps in memory only; a second call shows "This link is not valid" without revealing whether the envelope exists. Request a new session for every display.
- The link in the invitation email stays valid.
- The response is never stored (it contains a link): repeating a call with the same
Idempotency-Keycreates a new session; the key only protects against simultaneous duplicate calls (409 / 9008). - Treat the URL like a password: do not log it, do not pass it on, and do not create it in your browser application (only on your server).
- Every session appears in the audit trail of the envelope (
LINK_ISSUED, without the URL).
Possible errors, checked in this order:
| Status / code | Meaning |
|---|---|
| 404 | The envelope or recipient does not belong to your organization or not to the mode of the key |
| 403 / 9039 | Embedding is switched off for your organization |
| 403 / 9038 | origin is not on the list (also: a wildcard instead of an exact origin, http://localhost in live mode) |
422 / 9012 qes_not_embeddable | The recipient signs qualified (QES) |
| 409 / 9033 | The recipient is not up next (details.reason) |
409 / 9030 signing_page_disabled | The signing page is switched off |
403 / 9030 embedding_live_disabled | Live key: embedding is not yet released for live use |
| 403 | The key lacks envelopes:signing_urls |
A signing link (POST …/signing-url) or the link of the invitation email can be embedded as well: append ?embed=1. Then every website on your list may show the page, and the link works as usual (15 minutes until first opened, then a 60-minute session). The embedded session is preferable because it is bound to exactly one website.
3. Show the page with the widget
<div id="signature"></div>
<script src="https://sign.<domain>/embed/v1/onesign.js"></script>
<script>
const widget = OneSign.mountSigning(document.getElementById('signature'), {
url: sessionUrl, // the URL from step 2, from your server
minHeight: 600,
title: 'Sign the rental agreement',
onEvent(event) {
if (event.type === 'completed') showCompletion();
if (event.type === 'declined') showDeclined();
if (event.type === 'error') showError(event.code);
},
});
// widget.destroy() removes the iframe again.
</script>
The widget has no dependencies and sets no cookies. It creates the iframe with allow="camera; microphone; fullscreen" (the identity check of AES needs the camera at every frame level) and referrerpolicy="no-referrer", adjusts the height (resize) and passes on only messages that come from the origin of the signing page and match the protocol.
Without the widget: your own iframe with the same attributes works just as well; then listen for message yourself and check event.origin against the origin of the URL, as well as source and v of the message.
4. Messages of the page
Every message has the form { source: "onesign", v: 1, type, …, at } and never contains names, email addresses, tokens or links. The page sends them only to the bound origin (or the checked website of your list), never to *.
type | Additional fields | Meaning |
|---|---|---|
ready | envelopeId, recipientId | Page loaded |
resize | height | Height of the page in pixels |
signed | envelopeId, recipientId | The recipient has signed |
approved | envelopeId, recipientId | The recipient has approved |
viewed | envelopeId, recipientId | The recipient has confirmed viewing |
declined | envelopeId, recipientId | The envelope was declined (also in the signing step) |
completed | envelopeId | The envelope is complete |
expired | envelopeId, recipientId | The envelope has expired |
error | code | link_invalid, rate_limited, unavailable, action_failed, envelope_cancelled, envelope_failed, qes_new_window |
envelopeId and recipientId appear only in messages of an embedded session; with a signing link and ?embed=1 they are missing. In embedded mode the page never redirects on its own: react to completed (or the webhook). The privacy notice and the footer with the legal notice stay visible in the frame.
5. Security and browsers
- Clickjacking protection: The signing page allows embedding (
Content-Security-Policy: frame-ancestors) only for the bound origin or the websites on your list; without?embed=1it can never be embedded. - No cookies: Browsers block cookies in third-party frames (for example Safari). The page needs none: the session lives in the memory of the page.
- QES: If an embedded link opens the step of a member with a qualified signature, the page shows "Sign in in a new window" (opens a new window) and reports
errorwithqes_new_window. - Live use: In live mode, embedding is still blocked (403 / 9030
embedding_live_disabled) until it has been checked on real devices. In test mode everything is available.