Skip to main content

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

  1. Your backend creates an envelope (PDF + fields + signer) as usual.
  2. Your backend creates an embedded signing session for one signer.
  3. SignProof returns a one-time sessionToken + embedUrl.
  4. Your frontend renders the iframe (use @signproof/embed-react).
  5. The signer signs inside the iframe.
  6. 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"
}
FieldRequiredNotes
tenantIdyesMust own the envelope.
envelopeId, signerIdyesSigner must belong to the envelope; the envelope must be open (not completed/voided/declined/expired).
allowedOriginyesExact origin that may frame the signer. https only (http://localhost in dev).
expiresInMinutesnoDefault 30, max 120.
returnUrlnohttps URL surfaced to your app on completion.
themenoprimaryColor (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 *):
EventFires when
getsigned.signer.readyThe signer UI has mounted.
getsigned.signer.viewedThe signer is viewing the document/consent.
getsigned.signer.consentAcceptedE-sign consent accepted.
getsigned.signer.signatureAppliedA signature/initial was applied to a field.
getsigned.signer.completedThe signer finished.
getsigned.signer.declinedThe signer declined.
getsigned.signer.errorAn error occurred.
getsigned.signer.resizeContent 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

SymptomCause
401 on createMissing/invalid OAuth2 token.
404 on createEnvelope/signer not found for your app+tenant.
409 on createEnvelope completed/voided/declined/expired.
”Invalid link” in iframeToken wrong or already invalidated.
”Link expired” in iframeSession past expiresAt.
Iframe blank / CSP error in consolePage 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.