> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thesignproof.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedded Signing

> Embed the SignProof signing experience inside your own app with a short-lived session and an iframe — no signer email round-trip.

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

<Note>
  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](/guides/signature-generator) are the only
  parts that work fully client-side.
</Note>

***

## 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)

<Warning>
  Never create sessions from the browser. Your `client_secret` and bearer token
  must stay on your server.
</Warning>

```bash theme={null}
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:

```json theme={null}
{
  "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](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)

```bash theme={null}
npm install @signproof/embed-react
```

```tsx theme={null}
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](/guides/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.
