What embedded signing is
Embedded signing lets your users sign a SignProof envelope without leaving your
product. Your backend creates a short-lived signing session, your frontend
renders it in an iframe, and your app reacts to events via postMessage and
webhooks.
It reuses the exact same signing flow as emailed links — consent, identity
verification, signature capture, audit trail, and sealing are unchanged.
Embedded signing requires the API. The session must be created server-side with
your OAuth2 credentials. The signature generator (SignProofSignatureMaker)
and the public signature generator are the only
parts that work fully client-side.
The flow
- Your backend creates an envelope (PDF + fields + signer) as usual.
- Your backend creates an embedded signing session for one signer.
- SignProof returns a one-time
sessionToken + embedUrl.
- Your frontend renders the iframe (use
@signproof/embed-react).
- The signer signs inside the iframe.
- The iframe emits
postMessage events; SignProof fires your webhooks.
1. Create the session (server-side)
Never create sessions from the browser. Your client_secret and bearer token
must stay on your server.
curl -X POST https://api.thesignproof.com/v1/embedded/signing-sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "your-tenant",
"envelopeId": "ENVELOPE_UUID",
"signerId": "SIGNER_UUID",
"allowedOrigin": "https://app.yourcompany.com",
"expiresInMinutes": 30,
"returnUrl": "https://app.yourcompany.com/contracts/123",
"theme": { "primaryColor": "#111827", "borderRadius": "12px" }
}'
Response:
{
"sessionToken": "emb_sign_...",
"embedUrl": "https://embed.thesignproof.com/embed/sign/emb_sign_...",
"expiresAt": "2026-01-01T00:30:00Z"
}
| Field | Required | Notes |
|---|
tenantId | yes | Must own the envelope. |
envelopeId, signerId | yes | Signer must belong to the envelope; the envelope must be open (not completed/voided/declined/expired). |
allowedOrigin | yes | Exact origin that may frame the signer. https only (http://localhost in dev). |
expiresInMinutes | no | Default 30, max 120. |
returnUrl | no | https URL surfaced to your app on completion. |
theme | no | primaryColor (hex) and borderRadius only; everything else is ignored. |
The token is returned once and stored only as a hash. Pass sessionToken to
your frontend.
2. Render it (React)
npm install @signproof/embed-react
import { SignProofSigner } from '@signproof/embed-react';
export function SignStep({ sessionToken }: { sessionToken: string }) {
return (
<SignProofSigner
sessionToken={sessionToken}
onReady={() => console.log('signer loaded')}
onSigned={(e) => router.push(`/contracts/${e.envelopeId}/done`)}
onDeclined={() => toast('Signing declined')}
onError={() => toast('Could not load the signer')}
/>
);
}
The component validates message origins, maps events to callbacks, auto-resizes,
and cleans up its listener on unmount. You can also embed the embedUrl in a
plain <iframe> and listen for window.message yourself.
3. Events
The iframe posts these to your allowedOrigin (never *):
| Event | Fires when |
|---|
getsigned.signer.ready | The signer UI has mounted. |
getsigned.signer.viewed | The signer is viewing the document/consent. |
getsigned.signer.consentAccepted | E-sign consent accepted. |
getsigned.signer.signatureApplied | A signature/initial was applied to a field. |
getsigned.signer.completed | The signer finished. |
getsigned.signer.declined | The signer declined. |
getsigned.signer.error | An error occurred. |
getsigned.signer.resize | Content height changed ({ height }). |
Each event includes { envelopeId, signerId }.
4. Webhooks are the source of truth
postMessage events are for UI reactions only — a browser tab can close
before completed fires. For backend state (fulfilment, status, audit), rely on
the same envelope webhooks as any other signing:
envelope.completed, signer.signed, envelope.declined. See
Webhooks.
5. Security
- Sessions are short-lived, single-signer, single-origin, and stored as a hash.
- The browser enforces
Content-Security-Policy: frame-ancestors <allowedOrigin>
on the embed page — only your registered origin can frame it.
- Never expose your API key/secret in frontend code.
- Expired, revoked, completed, or wrong-origin sessions are rejected.
Local development
- Create the session with
"allowedOrigin": "http://localhost:3000" — plain
http://localhost is allowed only in development.
- Point the iframe at your local signing UI host (the
/embed/sign route).
Common errors
| Symptom | Cause |
|---|
401 on create | Missing/invalid OAuth2 token. |
404 on create | Envelope/signer not found for your app+tenant. |
409 on create | Envelope completed/voided/declined/expired. |
| ”Invalid link” in iframe | Token wrong or already invalidated. |
| ”Link expired” in iframe | Session past expiresAt. |
| Iframe blank / CSP error in console | Page origin ≠ the session’s allowedOrigin. |
Coming later: embedded builder
Embedded signing ships first. An embedded builder (place fields in your
own app) will reuse the same session + origin + theme model
(POST /v1/embedded/builder-sessions, /embed/builder/:token). Not available
yet — build signing first.