# Formable API — agent implementation spec

You are integrating Formable's embedded REST API into a product. Follow this document exactly. Do not invent endpoints, fields, or event names. Human docs: https://docs.formabledocs.com/. Read the live OpenAPI from https://formable-production.up.railway.app/docs/v1/ and https://formable-production.up.railway.app/docs/v0/. Use the Production server URL and paths listed there.

Formable is not a headless PDF toolkit. REST creates templates, signature requests, and redline requests. Formable returns short-lived URLs. You embed those URLs in an iframe. Formable renders the signing UI, redlining editor, or template field editor.

## Product

- Standalone app: https://app.formabledocs.com
- Marketing: https://www.formabledocs.com
- Developers: https://www.formabledocs.com/developers
- OpenAPI v1: https://formable-production.up.railway.app/docs/v1/
- OpenAPI v0: https://formable-production.up.railway.app/docs/v0/

Two embeddable products:

1. **E-signing API** — create a signature request, mint a `signingUrl` (`SignatureRequestUrl`), embed it. Formable handles fields, signature capture, progress, and the signed PDF.
2. **Redlining API** — create a redline request on a DOCX-backed template, mint a `redlineUrl` per member, embed it. Formable handles tracked changes, comments, turns, and accept/reject.

## Auth

All endpoints except `GET /health` and `GET /ai-service/health` require:

```
Authorization: Bearer <JWT>
```

Issue API credentials in the Formable dashboard (client id / secret → JWT). Send JSON as `Content-Type: application/json` unless uploading a file (`multipart/form-data`).

Errors are JSON `{ "error": "<message>" }` with `400`, `401`, `403`, `404`, `413`, or `500`.

## Test mode

Pass `"testMode": true` on create-signature-request and create-redline-request. Test traffic is not billed. Use it until you go live.

## Embedding

Mint a URL, then render:

```html
<iframe src="{signingUrl or redlineUrl or editUrl}" allow="clipboard-write"></iframe>
```

Signing URLs expire about 1 hour after mint. Template edit URLs expire about 1 day after mint. Mint again when expired. Do not build a custom signing or redlining frontend.

---

## Endpoint index

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| POST | `/templates` | Bearer | Upload PDF or DOCX, create template |
| POST | `/templates/{templateId}/edit-url` | Bearer | Mint template editor URL |
| POST | `/signature-requests` | Bearer | Create embedded signature request |
| GET | `/signature-requests` | Bearer | List signature requests |
| GET | `/signature-requests/{signatureRequestId}` | Bearer | Get one signature request |
| POST | `/signature-requests/{signatureRequestId}/url` | Bearer | Mint signing URL |
| GET | `/signature-requests/{signatureRequestId}/signed-envelope` | Bearer | Presigned URL for completed PDF |
| GET | `/signature-requests/{signatureRequestId}/events` | Bearer | Signing event history |
| POST | `/redline-requests` | Bearer | Create redline request |
| GET | `/redline-requests` | Bearer | List redline requests |
| GET | `/redline-requests/{redlineRequestId}` | Bearer | Get one redline request |
| PUT | `/redline-requests/{redlineRequestId}/members` | Bearer | Replace/upsert members |
| POST | `/redline-requests/{redlineRequestId}/url` | Bearer | Mint redline editor URL |
| GET | `/redline-requests/{redlineRequestId}/events` | Bearer | Redline event history |
| GET | `/billing` | Bearer | Redlining session usage |
| GET | `/health` | None | API health |
| GET | `/ai-service/health` | None | AI service health |

List endpoints accept optional `updatedSince` (ISO-8601). Only rows with `updated_at` strictly after that instant are returned.

---

## Templates

### POST `/templates`

Multipart form. Max file size 40MB. PDF or DOCX only.

| Field | Type | Required | Notes |
|-------|------|----------|-------|
| file | binary | yes | The document |
| filename | string | yes | Include extension, e.g. `offer.docx` |

Response `200`:

```json
{
  "templateId": "abc123xyz",
  "editTemplateAccess": {
    "editUrl": "https://app.formabledocs.com/template-setup/abc123xyz",
    "expiresAt": "2024-01-16T10:30:00.000Z"
  }
}
```

`413` if the file is too large. `400` if file or filename is missing.

### POST `/templates/{templateId}/edit-url`

No body. Response `200`:

```json
{
  "editUrl": "https://app.formabledocs.com/template-setup/abc123xyz",
  "expiresAt": "2024-01-16T10:30:00.000Z"
}
```

Embed `editUrl` so a human (or your admin UI) can place signature, text, date, and other fields. The template's field stack is what signers later fill.

---

## E-signing

Typical flow:

1. `POST /templates` (or reuse an existing `templateId`).
2. Optionally `POST /templates/{id}/edit-url` and place fields.
3. `POST /signature-requests` with signer, sender, optional field prefill.
4. `POST /signature-requests/{id}/url` → `signingUrl`.
5. Embed `signingUrl`.
6. Listen for webhooks or poll `GET .../events` and `GET .../{id}`.
7. When status is `Completed`, `GET .../signed-envelope`.

### POST `/signature-requests`

```json
{
  "templateId": "tmpl_offer_v1",
  "signer": {
    "email": "jane@acme.com",
    "name": "Jane Doe"
  },
  "sender": {
    "email": "alex@yourapp.com",
    "name": "Alex Chen"
  },
  "testMode": false,
  "fields": [
    { "fieldId": "field_123", "value": "Staff Engineer" }
  ]
}
```

| Field | Type | Required |
|-------|------|----------|
| templateId | string | yes |
| signer.email | email | yes |
| signer.name | string | yes |
| sender.email | email | yes |
| sender.name | string | yes |
| testMode | boolean | no, default false |
| fields | `{ fieldId, value }[]` | no, prefill template fields |

Response `200`:

```json
{ "signatureRequestId": "sr_abc123" }
```

### GET `/signature-requests` and GET `/signature-requests/{signatureRequestId}`

Object shape:

```json
{
  "signatureRequestId": "sr_abc123",
  "templateId": "tmpl_offer_v1",
  "signer": { "email": "jane@acme.com", "name": "Jane Doe" },
  "sender": { "email": "alex@yourapp.com", "name": "Alex Chen" },
  "status": "Created",
  "testMode": false
}
```

`status` is `Created` or `Completed`. List returns an array of these objects.

### POST `/signature-requests/{signatureRequestId}/url`

No body. Response `200`:

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

### GET `/signature-requests/{signatureRequestId}/signed-envelope`

Only when status is `Completed`. Response `200`:

```json
{
  "signedEnvelopePresignedUrl": "https://s3.amazonaws.com/.../signed-envelope.pdf?..."
}
```

### GET `/signature-requests/{signatureRequestId}/events`

Response `200`:

```json
{
  "signatureRequestEvents": [ /* event objects, same payload shape as webhooks */ ]
}
```

---

## Redlining

The template must have a DOCX source. PDF-only templates cannot start a redline request.

Typical flow:

1. `POST /templates` with a `.docx`.
2. `POST /redline-requests` with at least disclosing and receiving members.
3. `POST /redline-requests/{id}/url` with `memberEmail` for the party whose turn it is.
4. Embed `redlineUrl`.
5. Webhook or poll events. When `document_ready_for_signing` fires (or GET status is `DocumentReadyForSigning`), continue to e-signing if you need signatures.

Roles (request JSON uses PascalCase):

| Role | Meaning |
|------|---------|
| DisclosingParty | Originator of the contract |
| ReceivingParty | Counterparty |
| DisclosingCounsel | Counsel invited for the disclosing side |
| ReceivingCounsel | Counsel invited for the receiving side |

Webhook payloads use snake_case for the same roles: `disclosing_party`, `receiving_party`, `disclosing_counsel`, `receiving_counsel`.

### POST `/redline-requests`

```json
{
  "templateId": "tmpl_nda_v3",
  "members": [
    {
      "email": "counsel@acme.com",
      "displayName": "Alex Chen",
      "role": "DisclosingParty"
    },
    {
      "email": "legal@counterparty.com",
      "displayName": "Jordan Lee",
      "role": "ReceivingParty"
    }
  ],
  "testMode": false,
  "metadata": {
    "subject": "Acme × Counterparty NDA"
  }
}
```

| Field | Type | Required |
|-------|------|----------|
| templateId | string | yes |
| members | array, min 1 | yes; each needs email, displayName, role |
| testMode | boolean | no |
| metadata.subject | string | no |

Response `200`:

```json
{
  "redlineRequestId": "rr_abc123",
  "templateId": "tmpl_nda_v3_copy"
}
```

`templateId` in the response is the template created for this negotiation, which may differ from the source template id you posted.

### GET `/redline-requests` and GET `/redline-requests/{redlineRequestId}`

```json
{
  "templateId": "tmpl_nda_v3_copy",
  "status": "DisclosingPartyDraft",
  "members": [
    {
      "role": "DisclosingParty",
      "email": "counsel@acme.com",
      "displayName": "Alex Chen"
    }
  ],
  "testMode": false,
  "currentRound": "Disclosing"
}
```

`status` enum:

- `DisclosingPartyDraft`
- `DisclosingPartyRequestedReview`
- `ReceivingPartyDraft`
- `ReceivingPartyOpened`
- `ReceivingPartyRequestedReview`
- `DocumentReadyForSigning`

`currentRound` is `Disclosing` or `Receiving`.

### PUT `/redline-requests/{redlineRequestId}/members`

Replaces / upserts the member list. Same member objects as create.

```json
{
  "members": [
    {
      "email": "counsel@acme.com",
      "displayName": "Alex Chen",
      "role": "DisclosingParty"
    },
    {
      "email": "outside@firm.com",
      "displayName": "Sam Patel",
      "role": "DisclosingCounsel"
    }
  ]
}
```

Response `200`: `{ "members": [ /* updated members */ ] }`

### POST `/redline-requests/{redlineRequestId}/url`

```json
{ "memberEmail": "legal@counterparty.com" }
```

`memberEmail` is required and must already be a member. Response `200`:

```json
{
  "redlineUrl": "https://app.formabledocs.com/redlining/xyz789abc",
  "expiresAt": "2024-01-16T10:30:00.000Z"
}
```

### GET `/redline-requests/{redlineRequestId}/events`

Response `200`:

```json
{
  "redlineRequestEvents": [ /* event objects, same payload shape as webhooks */ ]
}
```

---

## Webhooks

Register one HTTPS endpoint per organization in the Formable dashboard. Formable POSTs JSON to that URL when signing or redlining events occur. Respond `200` within 10 seconds.

### Delivery

- Method: `POST`
- Header: `Content-Type: application/json`
- Header: `Content-Sha256: <hmac>`
- Body: the event payload only (see below). The body is **not** wrapped with `callback_url` or `secret`.

### Verifying `Content-Sha256`

The dashboard gives you a webhook signing secret (base64). Verify every request:

1. Decode the secret from base64 to bytes.
2. HMAC-SHA256 the **raw request body** (UTF-8) with that key.
3. Base64-encode the digest.
4. Compare to the `Content-Sha256` header with a constant-time compare.

Reject the request if the header is missing or the digest does not match.

### Envelope (every event)

```json
{
  "event": {
    "event_id": "n9c8123umxrs082rum",
    "event_category": "signing",
    "event_type": "document_signed",
    "event_time": 1700000000000
  }
}
```

`event_time` is milliseconds since epoch. `event_category` is `signing` or `redlining`.

### Signing events (`event_category`: `signing`)

The sibling object is `signing`.

| `event_type` | Extra fields on `signing` | When |
|--------------|---------------------------|------|
| `document_viewed` | `signature_request_id` | Signer opened the document |
| `document_signed` | `signature_request_id`, `recipient_signature_id` | That recipient signed |
| `document_completed` | `signature_request_id` | Envelope finished; fetch the signed PDF |

Example `document_signed`:

```json
{
  "event": {
    "event_id": "evt_01",
    "event_category": "signing",
    "event_type": "document_signed",
    "event_time": 1700000000000
  },
  "signing": {
    "signature_request_id": "sr_abc123",
    "recipient_signature_id": "rsig_456"
  }
}
```

On `document_completed`, call `GET /signature-requests/{signature_request_id}/signed-envelope`.

### Redlining events (`event_category`: `redlining`)

The sibling object is `redlining`. Every redlining payload includes:

```json
{
  "redline_request_id": "rr_abc123",
  "redline_member_role": "disclosing_party"
}
```

`redline_member_role` is one of `disclosing_party`, `receiving_party`, `disclosing_counsel`, `receiving_counsel`.

| `event_type` | Extra fields | When |
|--------------|--------------|------|
| `redline_shared` | (base only) | Request shared with members |
| `redline_edit_inserted` | `redline_edit_insertion` (string) | Insertion tracked change |
| `redline_edit_deleted` | `redline_edit_deletion` (string) | Deletion tracked change |
| `redline_edit_accepted` | `content`, `change_type` | Counterparty accepted a change |
| `redline_edit_rejected` | `content`, `change_type` | Counterparty rejected a change |
| `redline_comment_added` | `comment_added` (string) | Comment on the draft |
| `redline_message_added` | `message`, `author_email` | Turn message |
| `redline_turn_ended` | (base only) | Current party's turn ended |
| `document_ready_for_signing` | (base only) | Tracked changes resolved and template review done; ready to sign |

`change_type` is one of `inserted`, `deleted`, `replaced`, `formatted`.

Example `redline_comment_added`:

```json
{
  "event": {
    "event_id": "evt_02",
    "event_category": "redlining",
    "event_type": "redline_comment_added",
    "event_time": 1700000000000
  },
  "redlining": {
    "redline_request_id": "rr_abc123",
    "redline_member_role": "receiving_party",
    "comment_added": "Please cap liability at 12 months of fees."
  }
}
```

You can poll `GET .../events` instead of (or in addition to) webhooks while integrating.

---

## Other endpoints

### GET `/billing`

```json
{ "numberOfRedliningSessions": 42 }
```

### GET `/health`

```json
{
  "status": "healthy",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "uptime": 3600.5,
  "version": "1.0.0"
}
```

### GET `/ai-service/health`

```json
{ "status": "ai service healthy" }
```

---

## Implementation rules

1. Use the Production server URL from https://formable-production.up.railway.app/docs/v1/ or https://formable-production.up.railway.app/docs/v0/ and Bearer JWT. Do not call GraphQL or app cookies for the public API.
2. Create a template, then a signature request or redline request. Never skip to minting a URL without a request id.
3. Embed Formable URLs. Do not rebuild signing or redlining UI.
4. Prefer `testMode: true` until production.
5. Verify webhooks with HMAC-SHA256 + `Content-Sha256`. Do not trust unsigned POSTs.
6. Treat `document_completed` / `DocumentReadyForSigning` as the gates for downloading a signed PDF or starting e-sign after redline.
7. Field `fieldId` values come from the template you created. Do not invent field ids.
8. Redline member roles in REST bodies are PascalCase. The same roles in webhook JSON are snake_case.
9. If an OpenAPI field is not in this spec, do not use it.

## Minimal TypeScript signing example

Set `FORMABLE_API_BASE` to the Production server listed on https://formable-production.up.railway.app/docs/v1/ or https://formable-production.up.railway.app/docs/v0/.

```ts
const API = process.env.FORMABLE_API_BASE;
const headers = {
  Authorization: `Bearer ${process.env.FORMABLE_API_KEY}`,
  "Content-Type": "application/json",
};

const created = await fetch(`${API}/signature-requests`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    templateId: "tmpl_offer_v1",
    signer: { email: "jane@acme.com", name: "Jane Doe" },
    sender: { email: "alex@yourapp.com", name: "Alex Chen" },
    testMode: true,
  }),
});
const { signatureRequestId } = await created.json();

const minted = await fetch(`${API}/signature-requests/${signatureRequestId}/url`, {
  method: "POST",
  headers,
});
const { signingUrl, expiresAt } = await minted.json();
```

Embed `signingUrl` in an iframe. On webhook `document_completed`, GET `/signature-requests/{signatureRequestId}/signed-envelope`.

## Related

- OpenAPI v1: https://formable-production.up.railway.app/docs/v1/
- OpenAPI v0: https://formable-production.up.railway.app/docs/v0/
- Human docs: https://docs.formabledocs.com/
- Short site index: https://www.formabledocs.com/llms.txt
- Broader product reference: https://www.formabledocs.com/llms-full.txt
