# Sign a document in an iframe on your page

Embed the SecurySign signing iframe to collect passkey approval on your page. The backend hashes the document and requests a token for that hash. The page opens the frame with the token, hash and document name. After the customer reviews the request and approves it, the frame returns the signature through `postMessage`.

## What you need

Before embedding the frame, make sure the following are in place:

- An approved relying party (RP) and its Secure Signature Confirmation (SSC) secret, from the [RP dashboard](https://cloud.securysign.com/#/rp/dashboard). See the [RP integration guide](#/docs/rp-integration-guide).
- Every page that embeds the iframe served from an [authorised signing origin](#/docs/rp-integration-guide#3-authorise-additional-signing-origins).

## 1. Request a signing token

Compute the SHA-256 hash of the exact document bytes and encode it as a 64-character hexadecimal string. Request the token just before opening the frame so the customer has the full five-minute approval window. The example below uses `RP_CLIENT_ID` and `SSC_SECRET` from the dashboard, Python 3, curl and `sha256sum`, with `Contract.pdf` in the working directory:

```bash
export DOCUMENT_HASH="$(sha256sum Contract.pdf | cut -d ' ' -f 1)"
python3 - <<'JSON' > signing-token-request.json
import json, os
print(json.dumps({
    "clientId": os.environ["RP_CLIENT_ID"],
    "clientSecret": os.environ["SSC_SECRET"],
    "documentHash": os.environ["DOCUMENT_HASH"],
    "loa": "LOA-2"
}))
JSON
curl --fail-with-body -X POST https://securysign.com/api/ssc/token \
  -H "Content-Type: application/json" \
  --data-binary @signing-token-request.json -o signing-token-response.json
rm signing-token-request.json
```

| Field | Type | Required | Description |
|---|---|---|---|
| `clientId` | string | Yes | Your client ID, for example `signa-rp-42`. |
| `clientSecret` | string | Yes | Your SSC secret from the RP dashboard. It is a different value from your OpenID Connect (OIDC) client secret. |
| `documentHash` | string | Yes | The document's SHA-256 hash, in hex. |
| `loa` | string | No | The level of assurance (LOA): `LOA-0`, `LOA-1`, `LOA-2` (default) or `LOA-4`. With `LOA-2`, your user approves with any of their passkeys; with `LOA-4`, only the bound passkey works. |
| `email` | string | For `LOA-4` | The signer's email address. The token binds the signer's most recently registered passkey. |

```json
{ "token": "eyJhbGciOiJIUzI1NiIs…", "expiresAt": 1790000300, "loa": "LOA-2", "credentialBound": false }
```

Pass `token` to the browser with the submitted hash. `expiresAt` is the expiry time in Unix seconds, five minutes after issuance. For `LOA-4`, `credentialBound: true` confirms that the token is bound to one passkey. Issuing a token counts against the RP's [signing allowance](#/docs/limits).

## 2. Embed the iframe

```html
<iframe
  id="signa-frame"
  title="SecurySign signing"
  src="https://securysign.com/#/sign-frame?documentHash=a3f7c2d8e9b10000000000000000000000000000000000000000000000000000&documentName=Contract.pdf&mode=registered&rpOrigin=https%3A%2F%2Fapp.example.com&token=eyJhbGci…"
  width="100%" height="420"
  allow="publickey-credentials-get *"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>
```

Use `URLSearchParams` to encode `rpOrigin` and `documentName`. The `/#/sign-frame` route loads the signing screen, and `allow="publickey-credentials-get *"` enables its cross-origin WebAuthn prompt. The [URL parameters](#/docs/iframe#url-parameters) and [iframe attributes](#/docs/iframe#iframe-attributes) are listed below.

## 3. Handle the result

The frame sends signing and resize events to its parent page. Check `event.origin` and `event.source` before handling them, so only messages from the embedded SecurySign frame are accepted:

```js
const frame = document.getElementById("signa-frame");

window.addEventListener("message", (event) => {
  if (event.origin !== "https://securysign.com" || event.source !== frame.contentWindow) return;
  const msg = event.data;
  switch (msg.type) {
    case "SSC_SIGN_COMPLETE":
      // Send to your backend and store it against the document.
      fetch("/api/signatures", { method: "POST", body: JSON.stringify(msg) });
      break;
    case "SSC_SIGN_ERROR":
      showError(msg.error);
      break;
    case "SSC_CLOSE_FRAME":
      cancelSigning(); // user dismissed the frame
      break;
    case "SSC_RESIZE":
      frame.style.height = msg.height + "px"; // no dead space, no inner scrollbar
      break;
  }
});
```

Store `operationId`, `signatureBase64`, `documentHash`, `userVerified`, `timestamp` and `levelOfAssurance` with the document. Check the returned `documentHash` against the saved hash to confirm that the signature belongs to the file presented for approval.

## 4. Test the integration

Load the page on an authorized origin and select **Sign**. The passkey prompt should open, and approval should produce `SSC_SIGN_COMPLETE` with a non-empty `signatureBase64`. Check that `documentHash` and `levelOfAssurance` match the request.

## Drop-in component

The component below takes the document hash, name and token from the backend. It builds the iframe URL, adjusts the height on `SSC_RESIZE` and invokes a callback when signing completes, fails or is cancelled.

#### React

```tsx
import { useEffect, useRef, useState } from "react";

const SIGNA = "https://securysign.com";

type SignResult =
  | { type: "SSC_SIGN_COMPLETE"; operationId: string; signatureBase64: string; documentHash: string; levelOfAssurance: string }
  | { type: "SSC_SIGN_ERROR"; error: string }
  | { type: "SSC_CLOSE_FRAME" };

export function SigningFrame({ token, documentHash, documentName, onResult }: {
  token: string;
  documentHash: string;
  documentName: string;
  onResult: (result: SignResult) => void;
}) {
  const frame = useRef<HTMLIFrameElement>(null);
  const [height, setHeight] = useState(420);

  useEffect(() => {
    const onMessage = (event: MessageEvent) => {
      if (event.origin !== SIGNA || event.source !== frame.current?.contentWindow) return;
      if (event.data?.type === "SSC_RESIZE") return setHeight(event.data.height);
      if (["SSC_SIGN_COMPLETE", "SSC_SIGN_ERROR", "SSC_CLOSE_FRAME"].includes(event.data?.type)) onResult(event.data);
    };
    window.addEventListener("message", onMessage);
    return () => window.removeEventListener("message", onMessage);
  }, [onResult]);

  const params = new URLSearchParams({ documentHash, documentName, mode: "registered", rpOrigin: window.location.origin, token });
  return (
    <iframe
      ref={frame}
      title="SecurySign signing"
      src={`${SIGNA}/#/sign-frame?${params}`}
      style={{ width: "100%", height, border: 0 }}
      allow="publickey-credentials-get *"
      sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
    />
  );
}
```

#### HTML

```html
<iframe id="signa-frame" title="SecurySign signing" style="width:100%;height:420px;border:0"
  allow="publickey-credentials-get *"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups"></iframe>

<script>
  const SIGNA = "https://securysign.com";
  const frame = document.getElementById("signa-frame");

  function openSigning({ token, documentHash, documentName }) {
    const params = new URLSearchParams({ documentHash, documentName, mode: "registered", rpOrigin: location.origin, token });
    frame.src = `${SIGNA}/#/sign-frame?${params}`;
  }

  window.addEventListener("message", (event) => {
    if (event.origin !== SIGNA || event.source !== frame.contentWindow) return;
    const msg = event.data;
    if (msg.type === "SSC_RESIZE") frame.style.height = msg.height + "px";
    if (msg.type === "SSC_SIGN_COMPLETE") saveSignature(msg); // send to your backend
    if (msg.type === "SSC_SIGN_ERROR") showError(msg.error);
    if (msg.type === "SSC_CLOSE_FRAME") cancelSigning();
  });
</script>
```

## URL parameters

```text
https://securysign.com/#/sign-frame?documentHash=…&documentName=…&mode=registered&rpOrigin=…&token=…
```

The frame is served at `https://securysign.com/#/sign-frame` in production and `https://signa.dev.securysign.com/#/sign-frame` in the sandbox.

| Parameter | Required | What you send |
|---|---|---|
| `documentHash` | Yes | SHA-256 of the document, hex. Must match the token. |
| `documentName` | Yes | Shown to the signer. URL-encode it. |
| `mode` | Yes | `registered` with a token, or `anonymous` for level of assurance (LOA) 0 and 1 without a token |
| `rpOrigin` | Yes | Your page's origin, URL-encoded. It must be an authorised signing origin, and it is bound into the passkey challenge the signer approves. |
| `token` | In `registered` mode | The token from `POST /ssc/token` |

## Iframe attributes

| Attribute | Why you need it |
|---|---|
| `allow="publickey-credentials-get *"` | Enables WebAuthn in a cross-origin frame. Without it, your user sees no passkey prompt. |
| `allow-scripts` | The signing UI runs JavaScript |
| `allow-same-origin` | WebAuthn checks the frame's origin |
| `allow-forms` | The signing UI uses form controls |
| `allow-popups` | The frame may open dialogs |

## Events

Accept incoming messages only from `https://securysign.com` and the embedded frame's `contentWindow`. Use `https://securysign.com` as the target origin for messages sent to the frame.

| Event | Direction | When you get or send it |
|---|---|---|
| `SSC_SIGN_REQUEST` | Page to frame | Send it to start signing without URL parameters |
| `SSC_SIGN_COMPLETE` | Frame to page | The document is signed |
| `SSC_SIGN_ERROR` | Frame to page | Signing failed |
| `SSC_CLOSE_FRAME` | Frame to page | The user closed the frame. Treat it as a cancel. |
| `SSC_RESIZE` | Frame to page | The frame's rendered size changed |

### SSC_SIGN_REQUEST

When reusing a frame for several documents, send each new document through `SSC_SIGN_REQUEST`. In registered mode, include the token issued by the backend for that document hash.

```js
frame.contentWindow.postMessage({
  type: "SSC_SIGN_REQUEST",
  documentName: "Contract.pdf",
  documentHash: "a3f7c2d8e9b10000000000000000000000000000000000000000000000000000",
  mode: "registered",
  token: "eyJhbGci…",
}, "https://securysign.com");
```

### SSC_SIGN_COMPLETE

```json
{
  "type": "SSC_SIGN_COMPLETE",
  "operationId": "ssc_sig_42",
  "signatureBase64": "MEUCIQDxY…",
  "documentHash": "a3f7c2d8e9b10000000000000000000000000000000000000000000000000000",
  "userVerified": true,
  "timestamp": 1790000200,
  "mode": "registered",
  "levelOfAssurance": "LOA-2"
}
```

### SSC_SIGN_ERROR

```json
{ "type": "SSC_SIGN_ERROR", "error": "User verification required" }
```

### SSC_RESIZE

```json
{ "type": "SSC_RESIZE", "width": 512, "height": 511 }
```

The first resize event contains `width`; subsequent events update `height`. Apply the reported `height` to the iframe to fit the signing controls.

## Sign several documents with one approval

The batch signing frame presents a list of documents for a single passkey approval and returns an outcome for each document. SecurySign identifies the signer from the passkey assertion. As with single-document signing, the page must run on an [authorized signing origin](#/docs/rp-integration-guide#3-authorise-additional-signing-origins).

1. Embed the frame with your page's origin in `rpOrigin`:

   ```html
   <iframe
     id="securysign-batch"
     src="https://securysign.com/#/batch-sign-frame?rpOrigin=https%3A%2F%2Fapp.example.com"
     allow="publickey-credentials-get *"
     width="100%" height="420"></iframe>
   ```

2. Wait for `SSC_BATCH_READY`, then send your documents. Give each one an `id` you recognise, the document's SHA-256 hash in hex, and optionally a `name` your user will see:

   ```js
   const frame = document.getElementById("securysign-batch");
   window.addEventListener("message", (event) => {
     if (event.origin !== "https://securysign.com") return;
     const msg = event.data;
     if (msg.type === "SSC_BATCH_READY") {
       frame.contentWindow.postMessage({
         type: "SSC_BATCH_REQUEST",
         documents: [
           { id: "doc-1", hash: "a3f7c2d800000000000000000000000000000000000000000000000000000000", name: "Contract.pdf" },
           { id: "doc-2", hash: "9b2e41f000000000000000000000000000000000000000000000000000000000", name: "Annex A.pdf" },
         ],
       }, "https://securysign.com");
     }
     if (msg.type === "SSC_BATCH_COMPLETE") saveSignatures(msg.batchId, msg.results);
     if (msg.type === "SSC_SIGN_ERROR") showError(msg.error);
   });
   ```

   Alternatively, put the same JSON array in the URL's `documents` parameter, URL-encoded.

3. Your user selects **Sign N documents with one passkey**, where N is the number of documents, and approves. You receive:

   ```json
   {
     "type": "SSC_BATCH_COMPLETE",
     "batchId": "3f9c1a2e-5b7d-4e8a-9c1f-0a2b3c4d5e6f",
     "status": "completed",
     "documentCount": 2,
     "results": [
       { "documentId": "doc-1", "status": "signed", "signature": "MEUCIQDxY…" },
       { "documentId": "doc-2", "status": "signed", "signature": "MEQCIF3k…" }
     ]
   }
   ```

Match each entry in `results` to its document. The overall `status` is `completed` when every document succeeds; partially completed batches also arrive through `SSC_BATCH_COMPLETE`. Send `SSC_BATCH_REQUEST` from the origin specified in `rpOrigin`.

## Approve a request your backend created

For requests created through the [Hash signing API](#/docs/api-hash-signing), use `requestId` or `batchId` instead of a token: `https://securysign.com/#/sign-frame?requestId=<requestId>&rpOrigin=<your origin>`. Approval produces `SSC_SIGN_COMPLETE` or `SSC_BATCH_COMPLETE`. See [Show the request to your user](#/docs/api-hash-signing#2-show-the-request-to-your-user).

For a PDF prepared through the [PAdES signing API](#/docs/api-pades-signing), use `padesOperation=<operationId>`. The frame sends `SSC_PADES_APPROVED` with the passkey assertion, which the backend submits to finalize the PDF. See [Get your user's approval in the signing frame](#/docs/api-pades-signing#2-get-your-users-approval-in-the-signing-frame).

## Browser support

Test the iframe in Chrome, Edge, Safari and Firefox with the `allow` attribute above. The available passkeys and approval methods depend on the customer's browser and device; mobile devices use their biometric or screen-lock prompt.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `RP origin not authorized: …` | Authorise the origin on the RP dashboard and wait for approval |
| Blank frame | Check the `#/sign-frame` route and that the token has not expired |
| No passkey prompt | Add `allow="publickey-credentials-get *"` and serve the page over HTTPS. The user may not have a passkey yet. |
| `Invalid client credentials` | You sent the OpenID Connect (OIDC) client secret. Use the SSC secret. |
| `LOA-4 requires an email hint` | Add `email` to the token request |
| `No credential found for user` | Your LOA-4 signer has no passkey yet. Send them through [enrolment](#/docs/enrolment). |
| `Daily signing limit exceeded (…)` | Your RP used its signing allowance for the day; it resets at 00:00 UTC. See [Pricing and limits](#/docs/limits). |
| `SSC_SIGN_ERROR` with `Passkey signature verification failed` | The approval did not come from your signer's registered passkey. Ask the signer to try again with the same passkey. |

The [Signing tokens reference](#/docs/api-signing-tokens) contains the token, challenge and finalization schemas.

## How it works

```text
  Your backend          Your page                 SecurySign iframe
  1 POST /ssc/token ──► 2 iframe src + token ───► 3 passkey prompt
                        4 SSC_SIGN_COMPLETE  ◄─── signature
```
