# Sign a PDF from your backend

PDF signing has three steps: prepare the document, collect passkey approval and finalize the signature. The prepare request returns an `operationId` for the signing frame. Once the customer approves the prepared hash, the backend submits the assertion to finalize and receives the PDF with its embedded PDF Advanced Electronic Signature (PAdES) and signer certificate.

For signing a document hash instead of a PDF, see the [Hash signing API](#/docs/api-hash-signing).

## What you need

This flow requires:

- A signed-in user's access token from [single sign-on](#/docs/sso).
- A user who has completed [enrolment](#/docs/enrolment), so they hold a passkey and a signing certificate.
- A page on an [authorised signing origin](#/docs/rp-integration-guide#3-authorise-additional-signing-origins) to show the signing frame on.
- A PDF of up to about 15 MB, because you can send at most 20 MB and base64 encoding adds about a third to the file size.

## 1. Prepare the PDF

Set `USER_ACCESS_TOKEN` to the customer's access token from sign-in. This Python 3 and curl example reads `Contract.pdf` from the working directory and saves the prepare response as `pdf-prepared.json`:

```bash
python3 - <<'JSON' > pdf-prepare.json
import base64, json
from pathlib import Path
print(json.dumps({
    "pdfBase64": base64.b64encode(Path("Contract.pdf").read_bytes()).decode(),
    "options": {"reason": "Contract acceptance", "location": "Nairobi"}
}))
JSON
curl --fail-with-body -X POST https://securysign.com/api/sign/pades/prepare \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @pdf-prepare.json -o pdf-prepared.json
```

```json
{
  "operationId": "op_7c1e0a",
  "hash": "5f2b9c0000000000000000000000000000000000000000000000000000000000",
  "certBase64": "MIIC…",
  "algorithm": "SHA256withECDSA",
  "credentialID": "signa_prod_1042_0"
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `pdfBase64` | string | Yes | The PDF, base64-encoded, for example with `base64 -w0 contract.pdf`. |
| `options.reason` | string | No | The reason shown in the PDF's signature panel. |
| `options.location` | string | No | The location shown in the PDF's signature panel. |

Save `operationId` and `hash` on the backend. The hash is the hexadecimal SHA-256 digest of the PDF byte range to be signed; `certBase64` contains the signer certificate, and `credentialID` identifies the server-side signing key. Approval and finalization must complete within five minutes.

## 2. Get your user's approval in the signing frame

Open the signing frame with `padesOperation=<operationId>`. The customer reviews the prepared hash and approves with a passkey, using the device's biometric or PIN prompt:

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

The page must run on an [authorized signing origin](#/docs/rp-integration-guide#3-authorise-additional-signing-origins), specified in the URL-encoded `rpOrigin`. Forward the approval returned by the frame to the backend for finalization:

```js
window.addEventListener("message", (event) => {
  const frame = document.getElementById("signing-frame");
  if (event.origin !== "https://securysign.com" || event.source !== frame.contentWindow) return;
  const msg = event.data;
  if (msg.type === "SSC_PADES_APPROVED") sendToBackend(msg.operationId, msg.assertion);
  if (msg.type === "SSC_SIGN_ERROR") showError(msg.error);
});
```

The `SSC_PADES_APPROVED` message contains `operationId` and the WebAuthn `assertion`, including `id`, `response.authenticatorData`, `response.clientDataJSON` and `response.signature`. If the customer cancels the prompt, the operation remains available for another approval until it expires.

## 3. Finalize and receive the signed PDF

Build the finalize request from that event: `credentialId` comes from `assertion.id`, and the three response values are copied into the fields below. Use the same customer's access token. This shell example assumes that `sendToBackend` has saved the full approval event as `pdf-approval.json`:

```bash
python3 - <<'JSON' > pdf-finalize.json
import json
from pathlib import Path
approval = json.loads(Path("pdf-approval.json").read_text())
a = approval["assertion"]
print(json.dumps({
    "operationId": approval["operationId"], "credentialId": a["id"],
    "signatureBase64": a["response"]["signature"],
    "authenticatorData": a["response"]["authenticatorData"],
    "clientDataJSON": a["response"]["clientDataJSON"]
}))
JSON
curl --fail-with-body -X POST https://securysign.com/api/sign/pades/finalize \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @pdf-finalize.json -o pdf-finalized.json
python3 - <<'PDF'
import base64, json
from pathlib import Path
result = json.loads(Path("pdf-finalized.json").read_text())
Path("Contract-signed.pdf").write_bytes(base64.b64decode(result["signedPdfBase64"]))
PDF
```

```json
{ "signedPdfBase64": "JVBERi0xLjc…", "operationId": "op_7c1e0a", "credentialID": "signa_prod_1042_0" }
```

Decode `signedPdfBase64` to recover the signed PDF, and store it with `operationId` and `credentialID`. Certificate validation requires a trust anchor; see the [Verification API](#/docs/api-certificates#2-validate-against-the-certificate-authority) for the certificate-authority checks.

## Troubleshooting

| Status | Message | Fix |
|---|---|---|
| `400` | `pdfBase64 required` | Send the PDF as base64 in `pdfBase64` |
| `400` | `Approve the signature with your passkey: …` | Send `signatureBase64`, `authenticatorData` and `clientDataJSON` from the assertion |
| `401` | `Invalid or expired token` | Send a current access token from a signed-in user |
| `403` | `Challenge mismatch — dynamic linking failed` | Use the bytes of `hash` from this operation as the challenge |
| `403` | `Passkey signature verification failed` | Have one of this user's own passkeys make the assertion |
| `403` | `Origin mismatch — possible phishing attack` | Get the approval in the signing frame (step 2) |
| `403` | `RP origin not authorized: …` (in the frame) | [Authorise the origin](#/docs/rp-integration-guide#3-authorise-additional-signing-origins) |
| `409` | `This PDF is already signed. …` (in the frame) | Prepare the PDF again |
| `404` | `Operation not found or access denied` | The operation is already finalized or belongs to another user. Prepare again. |
| `410` | `This signing operation expired. …` | You waited more than 5 minutes. Prepare again. |
| `413` | Request too large | Your body is over 20 MB. Send a smaller PDF. |

See the [endpoint schemas](#/docs/api-pades-signing#endpoints) for complete prepare and finalize requests, or the [PAdES workflow](#/docs/pades-signing) for the web application.
