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

# Embedded signing

> Let users sign documents inside your app with an embeddable signing URL.

Give your users the ability to sign documents directly in your product. Formable returns a short-lived signing URL that you load in an iframe — no email round-trip required.

This walkthrough covers the full embedded signing flow: create a signature request on your server, embed the signing experience on the client, detect completion, and download the signed PDF.

<Info>
  Need a shorter path first? Complete the [quickstart](/quickstart), then return here for the full integration pattern.
</Info>

<Info>
  Prefer email delivery instead of an iframe? See [Non-embedded signing](/walkthroughs/non-embedded-signing).
</Info>

## Prerequisites

Before you begin:

1. Create an organization and [API key](/authentication) in [Settings](https://app.formabledocs.com/settings).
2. Upload a template, place at least one required signature field, and assign it a signer role — see the [Embedded templates walkthrough](/walkthroughs/embedded-templates).
3. Call the Formable API from your **backend** only. Never expose your API key in the browser.

## Overview

<Steps>
  <Step title="Create an embedded signature request">
    Start a request from a template with one or more signers (each with a role).
  </Step>

  <Step title="Generate a signing URL">
    Mint a short-lived URL for a signer's `recipientSignatureId` just before they are ready.
  </Step>

  <Step title="Embed the URL in an iframe">
    Load the signing experience inside your product.
  </Step>

  <Step title="Detect completion and download">
    Listen for webhooks or poll status, then fetch the signed PDF.
  </Step>
</Steps>

## Server side

Create the signature request and signing URL on your backend, then pass only the URL to the client.

### 1. Create an embedded signature request

Start from a template you've already prepared. An embedded signature request needs:

* `templateId` — which document to send
* `signers` — who will sign (`email`, `name`, and `role` for every role used by required fields)

`sender` is optional. When omitted, Formable uses the API key organization owner.

```bash theme={null}
curl --request POST \
  --url https://api.formabledocs.com/v1/signature-requests/embedded \
  --header "Authorization: Bearer $TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
    "templateId": "abc123xyz",
    "signers": [
      { "email": "signer@example.com", "name": "Jane Doe", "role": "Client" }
    ],
    "sender": { "email": "you@yourcompany.com", "name": "Your Company" },
    "testMode": true
  }'
```

```json Response theme={null}
{
  "signatureRequestId": "sr_456def",
  "templateId": "abc123xyz",
  "signers": [
    {
      "email": "signer@example.com",
      "name": "Jane Doe",
      "recipientSignatureId": "rsig_789ghi"
    }
  ],
  "sender": { "email": "you@yourcompany.com", "name": "Your Company" },
  "status": "Created",
  "testMode": true
}
```

Save the `signatureRequestId` for status and download. Save each signer's `recipientSignatureId` to mint their signing URL.

#### Prefill fields

Templates can include fields beyond the signature — company name, dates, checkboxes, and so on. Pass a `fields` array to set values before the signer opens the document. Each entry sets a `value` on a `fieldId` from the template.

```json theme={null}
{
  "templateId": "abc123xyz",
  "signers": [
    { "email": "signer@example.com", "name": "Jane Doe", "role": "Client" }
  ],
  "sender": { "email": "you@yourcompany.com", "name": "Your Company" },
  "fields": [
    { "fieldId": "field_company_name", "value": "Acme Corporation" },
    { "fieldId": "field_effective_date", "value": "2024-02-01" }
  ],
  "testMode": true
}
```

<Info>
  Field IDs come from the template. Open the template's [editor URL](/walkthroughs/embedded-templates) to place fields and copy their IDs. A `fieldId` that doesn't exist on the template returns a `400`.
</Info>

#### Test mode

Set `testMode: true` while integrating so the request doesn't count toward billing. Test mode documents are watermarked and are not legally binding.

### 2. Generate a signing URL

A **signing URL** opens the signing experience for one recipient. Create it with that signer's `recipientSignatureId`. It expires **one hour** after creation, so generate it right before you show it to the signer.

```bash theme={null}
curl --request POST \
  --url https://api.formabledocs.com/v1/recipient-signatures/rsig_789ghi/url \
  --header "Authorization: Bearer $TOKEN"
```

```json Response theme={null}
{
  "signingUrl": "https://app.formabledocs.com/sign/embedded/xyz789abc",
  "expiresAt": "2024-01-16T10:30:00.000Z"
}
```

<Warning>
  Creating a signing URL for an already completed request returns `409`.
</Warning>

## Client side

Embed the `signingUrl` in an iframe so the signer can complete the document without leaving your product.

```html theme={null}
<iframe
  src="https://app.formabledocs.com/sign/embedded/xyz789abc"
  width="100%"
  height="800"
  allow="fullscreen"
  style="border: none;"
></iframe>
```

Request the URL from your backend and pass it to the page just before rendering:

<CodeGroup>
  ```javascript React theme={null}
  function SigningFrame({ recipientSignatureId }) {
    const [signingUrl, setSigningUrl] = useState(null);

    useEffect(() => {
      fetch(`/api/formable/signing-url?recipientSignatureId=${recipientSignatureId}`)
        .then((res) => res.json())
        .then((data) => setSigningUrl(data.signingUrl));
    }, [recipientSignatureId]);

    if (!signingUrl) return <p>Loading…</p>;

    return (
      <iframe
        src={signingUrl}
        width="100%"
        height="800"
        allow="fullscreen"
        style={{ border: "none" }}
        title="Sign document"
      />
    );
  }
  ```

  ```html Plain HTML theme={null}
  <iframe
    src="{{ signingUrl }}"
    width="100%"
    height="800"
    allow="fullscreen"
    style="border: none;"
  ></iframe>
  ```
</CodeGroup>

<Warning>
  Never call the Formable API with your token from the browser. Generate the signing URL on your server and hand only the resulting URL to the client.
</Warning>

<Tip>
  Give the iframe enough height (around `800px`) so signers don't have to scroll within a small frame. Regenerate the URL if the signer returns after it has expired.
</Tip>

## Signing experience

Once the iframe loads, the signer walks through required fields on the document.

Click **Start** to begin. Required fields are highlighted so the signer knows where to act.

<Frame caption="The signing experience with a required Signature field highlighted. Click Start to begin.">
  <img src="https://mintcdn.com/formable/fjyyUfRqAbymxgEh/static/images/embedded-signing/signing-start.png?fit=max&auto=format&n=fjyyUfRqAbymxgEh&q=85&s=f629ad3498f3bee55268f50e9e611a3a" alt="Embedded signing view showing document.docx with a Sign here field on the Customer line, a Required callout, and a Start button" width="2372" height="1740" data-path="static/images/embedded-signing/signing-start.png" />
</Frame>

When the signer reaches a Signature field, they can **Draw**, **Type**, or use a **Saved** signature. They must agree to the terms and conditions before confirming.

<Frame caption="The signature modal with Draw selected. Signers can also type a signature or reuse a saved one.">
  <img src="https://mintcdn.com/formable/-AZ0lJCayPXO7UKC/static/images/embedded-signing/signing-signature.png?fit=max&auto=format&n=-AZ0lJCayPXO7UKC&q=85&s=39357309053bc1fe32a5275d1f1d5d04" alt="Signature modal over the document with Draw, Type, and Saved tabs, a drawn signature, consent checkbox, and Confirm button" width="2406" height="1748" data-path="static/images/embedded-signing/signing-signature.png" />
</Frame>

After all required fields are complete, the signer reviews the document and clicks **Finish**. That completes the signature request and fires `onSigningComplete` to the parent window.

<Frame caption="Review step after the signature is placed. Click Finish to complete the request.">
  <img src="https://mintcdn.com/formable/-AZ0lJCayPXO7UKC/static/images/embedded-signing/signing-finish.png?fit=max&auto=format&n=-AZ0lJCayPXO7UKC&q=85&s=fd26158568755320cd35aa4de6363c06" alt="Embedded signing view with a completed Customer signature and a Finish button" width="2406" height="1754" data-path="static/images/embedded-signing/signing-finish.png" />
</Frame>

## Detect completion

When the signer finishes, the signature request `status` becomes `Completed`. You have four ways to learn that:

<AccordionGroup>
  <Accordion title="Listen for iframe postMessages" icon="window-maximize">
    When signing finishes inside the embedded iframe, Formable posts a message to the parent window. Use this to close the iframe or update your UI immediately:

    ```javascript theme={null}
    window.addEventListener("message", (event) => {
      if (event.origin !== "https://app.formabledocs.com") return;
      if (!event.data || typeof event.data !== "object") return;

      switch (event.data.type) {
        case "onSigningComplete":
          // Signer finished — close the iframe or show a success state
          break;
        case "onSigningError":
          // Signing failed to complete — show an error or retry
          break;
      }
    });
    ```

    | Message `type`      | When it fires                                   |
    | ------------------- | ----------------------------------------------- |
    | `onSigningComplete` | The signer successfully completed the document. |
    | `onSigningError`    | Signing could not be completed from the iframe. |

    Always check `event.origin`. Treat these as client UX signals — confirm completion with a webhook or API poll before downloading the signed PDF.
  </Accordion>

  <Accordion title="Listen for the document_completed webhook" icon="bell">
    Configure a [webhook endpoint](/webhooks/overview) and handle the [`document_completed`](/webhooks/events#document_completed) event. Formable pushes this once the signed PDF is ready, so you can download without polling. Do not use [`document_signed`](/webhooks/events#document_signed) alone — that fires when a signer finishes, before the completed file is available.

    ```json theme={null}
    {
      "event": {
        "event_type": "document_completed",
        "event_category": "signing"
      },
      "signing": {
        "signature_request_id": "sr_456def"
      }
    }
    ```
  </Accordion>

  <Accordion title="Poll the signature request status" icon="magnifying-glass">
    Fetch the [signature request](/api-reference/endpoint/get-signature-request) and check `status`. It moves from `Created` to `Completed`. The response also includes `signers` (with `recipientSignatureId`) and envelope `fields`.

    ```bash theme={null}
    curl --request GET \
      --url https://api.formabledocs.com/v1/signature-requests/sr_456def \
      --header "Authorization: Bearer $TOKEN"
    ```
  </Accordion>

  <Accordion title="Read the event stream" icon="list">
    Fetch [signature request events](/api-reference/endpoint/get-signature-request-events) for a chronological history of what happened.

    ```bash theme={null}
    curl --request GET \
      --url https://api.formabledocs.com/v1/signature-requests/sr_456def/events \
      --header "Authorization: Bearer $TOKEN"
    ```
  </Accordion>
</AccordionGroup>

<Tip>
  Use iframe `postMessage` for immediate UI updates, and prefer the `document_completed` webhook as the source of truth before downloading. Use [List signature requests](/api-reference/endpoint/list-signature-requests) with `updatedSince`, or the event stream, to reconcile anything you missed.
</Tip>

## Download the signed PDF

Once status is `Completed`, fetch a temporary download URL for the signed PDF (the **signed envelope**).

```bash theme={null}
curl --request GET \
  --url https://api.formabledocs.com/v1/signature-requests/sr_456def/signed-envelope \
  --header "Authorization: Bearer $TOKEN"
```

```json Response theme={null}
{
  "signedEnvelopePresignedUrl": "https://s3.amazonaws.com/bucket/signed-envelope.pdf?..."
}
```

Open or download that URL promptly — it is a short-lived presigned link. The signed PDF includes an audit trail with timestamps, actors, and IP addresses for each step.

<Frame caption="Audit trail appended to the signed PDF, showing Created, Sent, Signed, and Completed events.">
  <img src="https://mintcdn.com/formable/KjkDmUUIhYVtehc9/static/images/embedded-signing/signed-doc-with-audit-trail.png?fit=max&auto=format&n=KjkDmUUIhYVtehc9&q=85&s=72eaaabeb0c6818b5e52c02cd3575423" alt="Formable audit trail page for document.docx with Envelope ID, Completed status, and a Document History of Created, Sent, Signed, and Completed events" width="1634" height="1254" data-path="static/images/embedded-signing/signed-doc-with-audit-trail.png" />
</Frame>

<Note>
  Requesting the signed envelope before signing is complete returns `409` with `"Envelope has not been signed yet"`.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Embedded templates" icon="file-lines" href="/walkthroughs/embedded-templates">
    Upload documents and place signature and form fields.
  </Card>

  <Card title="Webhooks" icon="bell" href="/webhooks/overview">
    Get notified when a document is viewed or signed.
  </Card>

  <Card title="Redlining" icon="pen-line" href="/walkthroughs/redlining">
    Negotiate a DOCX contract before signing.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Full endpoint docs for signature requests.
  </Card>
</CardGroup>
