# Embedded signing

> Put the signer view inside your own product — URLs, the iframe, the message protocol, expiry, branding, and branding options.

Source: https://signclad.com/docs/embedded-signing/

Embedded signing puts the signer view in an iframe in your product, so your users never see a Signclad-branded page or leave your application.

## Getting a URL

Set `embedded_signing: true` when creating the document. Each recipient comes back with an `embedded_signing_url`:

```json
{
  "recipients": [
    { "id": "signer-1", "name": "Jane Doe", "email": "jane@example.com",
      "status": "sent",
      "embedded_signing_url": "https://app.signclad.com/sign/embedded/qN4vR7xJ2mLd...W8kZ" }
  ]
}
```

If you lose it, or it expires, mint a new one. You do **not** need to recreate the document:

```bash
curl -X POST \
  https://api.signclad.com/api/v1/documents/{id}/recipients/signer-1/signing_url \
  -H "X-Api-Key: $SIGNCLAD_API_KEY"
```

```json
{ "embedded_signing_url": "https://app.signclad.com/sign/embedded/7hB2pK9sVc4n...T1yQ",
  "signing_url": "https://app.signclad.com/sign/Lm3xD8fW5qZa...R6nJ",
  "expires_at": "2026-09-09T18:44:10Z" }
```

The token is sixty-four opaque characters and it is the **last path segment**, not a query parameter. Nothing about the document or the recipient is derivable from it.

Both forms come back on every call: `embedded_signing_url` for the iframe, `signing_url` for the hosted page you would put in your own email or a redirect. Regenerating supersedes the recipient's earlier links, so whatever you replaced stops working — which is the point of a regenerate button.

The embedded link is **single use** and has to be opened within **thirty minutes**; `expires_at` is that deadline. The hosted `signing_url` is reusable and lives as long as the document does. So mint the embedded one when the user asks to sign, not when you create the document — a URL that sat in your database for a week is a URL you have to handle expiring.

Also useful: `embedded_preview_url` on the document shows the whole document read-only, for a "review before signing" screen or for a party who is not a signer. It is `null` while the document is a draft and carries a URL once it has been sent. The URL carries its own read-only credential, so you can frame it for staff who have no Signclad account; treat it like any other signing link and do not publish it.

## Embedding it

```html
<iframe
  src="https://app.signclad.com/sign/embedded/qN4vR7xJ2mLd...W8kZ"
  allow="clipboard-read; clipboard-write"
  style="width: 100%; height: 800px; border: 0"
  title="Sign the agreement"
></iframe>
```

Give it real height. The signer view is a document; 800 pixels is a floor, not a target. That `allow` list is what the embed script puts on its own iframe by default (its `iframeAllow` option overrides it); nothing in the signer view asks for a camera or microphone, so do not delegate them — `clipboard-write` is the one the copy-a-link controls use.

The embed script appends `signclad_embedded_iframe=1` to the URL and starts a **20 second timer**. If `load` has not fired by then it raises `SC-document.error` with `errorCode: "request_timed_out"`, so a blocked iframe surfaces as an error rather than as a blank box forever.

## Messages

The iframe posts messages to the parent window. Listen and check the origin:

```js
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://app.signclad.com') return;

  switch (event.data.type) {
    case 'SC-document.setId':          onReady(event.data); break;
    case 'SC-document.documentLoaded': hideSkeleton(); break;
    case 'SC-document.completed':      onSigned(event.data); break;
    case 'SC-document.declined':       onDeclined(event.data); break;
    case 'SC-document.closed':         closeModal(); break;
    case 'SC-document.error':          onError(event.data); break;
  }
});
```

The payload is a flat `{type, …}` object — the fields are on `event.data` itself, not on a nested `payload`.

| Message | Payload | When |
|---|---|---|
| `SC-document.setId` | `{id, apiAppStyles, newEditUrl, view}` | Once, as soon as the document is known. Nothing reaches you before this. |
| `SC-document.documentLoaded` | `{id}` | The document is rendered and interactive. |
| `SC-document.completed` | `{id, redirect, url}` | This recipient finished. Others may still be outstanding. |
| `SC-document.declined` | `{id, declineReason, url}` | This recipient declined, with the reason. |
| `SC-document.closed` | `{id}` | The user asked to close the embedded view. |
| `SC-document.clickedContinueBtn` | `{id, url}` | The user pressed the primary continue action. |
| `SC-document.error` | `{id, errorCode, errorMessage}` | Something went wrong. `errorCode` is stable; `errorMessage` is for humans. |
| `SC-Embedder.addLoader` / `.removeLoader` | `{}` | Show or hide your own loading overlay. |

For a signing view, `view` in `setId` is `recipient_side`.

An expired link does not get its own message: it arrives as `SC-document.error` with an error code, and the iframe shows a reason page. So does a session that has been revoked.

**Treat these as hints, not as truth.** A browser message is not a durable record: the tab may close before it fires. Webhooks are the source of truth for "did this get signed"; the messages are for updating your own UI promptly.

Your page can send messages back too — `SC-Embedder-SetAttr` with the options, `SC-Embedder.resizing`, `SC-Embedder.clickAway`, and `SC-document.completeSelfSigning`. The full table, including the flags `SetAttr` carries, is in [embedded builders](https://signclad.com/docs/embedded-builders/#messages-your-page--iframe).

## Branding

Create an API application and bind it to your key:

```json
{
  "name": "Acme Lending portal",
  "primary_color": "#1F6FEB",
  "button_text_color": "#FFFFFF",
  "link_text_color": "#1F6FEB",
  "buttons_border_radius": 8,
  "callback_urls": ["https://portal.acme.example"]
}
```

The logo is a separate upload — `POST /api_applications/{id}/logo` — not a URL in this body. It comes back on the application as a short-lived signed URL.

`callback_urls` entries are validated as **origins**: scheme and host, optionally a port, and nothing else. A bare host is read as `https`; a path, a query, or a wildcard is a `422` rather than a silent trim.

Pass `api_application_id` on the create and the embedded surfaces carry your branding. There is no embed allowlist to configure: an embedded page addresses your host using the origin the browser reports for your own first message, so framing works wherever you serve it from.

Branding resolves per value: the application first, then the workspace's own branding, then the Signclad default, so an application that sets only `primary_color` keeps the workspace's logo. Custom branding is plan-gated — on a plan without it the application's styles are ignored and the defaults render.

Use `custom_requester_name` and `custom_requester_email` so the document reads as coming from your customer rather than from you. Neither changes the display name on the invitation email, which stays the workspace's.

## Emails

An embedded document is silent by default. Your application is opening the iframe, so Signclad
does not also mail the recipient a "please sign" invitation, and it does not run the automatic
reminders on days 3, 6 and 10. Set `embedded_signing_notifications: true` on the create if you
want those anyway, in which case each recipient's own `send_email` still applies.

Two things are not covered by that default, and are worth knowing before you build your own:

- **A reminder you ask for is sent.** `POST /documents/{id}/remind` mails the recipient whatever
  `embedded_signing_notifications` is set to. The default is about prompts Signclad decides to
  send on its own; a call you made is not one of those. The response's `reminded` names who was
  actually reached, which can be nobody, so read it rather than the status code.
- **Everything that is not a prompt still goes out.** Passcodes, the completed copy, and the
  sender's own decline, bounce, complaint and expiry notices are unaffected. The switch that
  silences all mail to one person is their `send_email: false`.

A recipient is never reminded once they have finished, declined or been reassigned, and a `cc`
is never reminded at all — naming them explicitly does not change that.

## Expiry and errors

| Situation | What you get | What to do |
|---|---|---|
| The URL expired | `SC-document.error` with an expiry code, and the iframe shows a reason page | Mint a new URL |
| The document was cancelled | A reason page | Nothing; it is over |
| Another recipient must sign first | A waiting page | Nothing; they will be invited automatically |
| The recipient already signed | A completed page with the download | Nothing |

Signer sessions are thirty minutes, sliding, and scoped to one recipient of one document.

## Related

- [Embedded builders and the compatibility shim](https://signclad.com/docs/embedded-builders/)
- [Webhooks](https://signclad.com/docs/webhooks/)
- [Migrating to Signclad](https://signclad.com/docs/migration/)
