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

# Documents API

> Endpoints for Document OS: generation, e-signature, obligations, semantic search, AI draft, and clickwrap consent.

The Documents API is the programmatic surface behind [Document OS](/documents/overview). It has two layers:

* **Authenticated routes** — bearer-token, org-scoped. Used by the in-app Document OS UI and your own integrations.
* **Public routes** — unauthenticated, used by the signer page and the clickwrap consent widget.

All authenticated endpoints follow the same conventions as the rest of the Winnerr API. See [API Introduction](/api-reference/introduction) for base URLs, bearer-token forwarding, and standard `401` behavior.

## Authentication

Authenticated routes expect the session bearer token forwarded by `apps/app`:

```ts theme={null}
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/documents/usage`, {
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
});
```

Every authenticated route is org-scoped—you can only read and write documents that belong to your organization.

Public routes (`/api/public/sign/:token` and `/api/public/clickwrap/:agreementId`) require no auth. The capability is the token or agreement ID embedded in the URL.

***

## Document usage

### `GET /api/documents/usage`

Returns the current calendar-month document usage for the organization.

**Response:**

```json theme={null}
{
  "used": 12,
  "included": 15,
  "periodLabel": "June 2026"
}
```

| Field         | Type            | Description                                                                                                                                      |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `used`        | integer         | Documents rendered (generated) this calendar month.                                                                                              |
| `included`    | integer or null | The plan's monthly allotment from `DOCUMENTS_INCLUDED_PER_MONTH`. `null` when unset (the meter still counts, but the allotment is not enforced). |
| `periodLabel` | string          | Human-readable month label, e.g. `"June 2026"`.                                                                                                  |

Generation is never blocked by the meter—going over `included` triggers overage billing, not a hard gate.

***

## AI draft

### `POST /api/documents/draft`

Generate a Zod-validated canonical draft from a natural-language prompt. The AI proposes; a human reviews in the wizard before generation. The response is a pre-filled canonical suitable for wizard pre-fill, never a completed document.

**Requires the AI gateway to be configured** (`@repo/ai` + a chat model in `models.json`).

**Request body:**

```json theme={null}
{
  "prompt": "Draft a condo lease for 88 Harbour St unit 2502, $3,200/month, one year from August 1 2026"
}
```

| Field    | Type   | Required | Description                                   |
| -------- | ------ | -------- | --------------------------------------------- |
| `prompt` | string | Yes      | Natural-language description of the document. |

**Response (success):**

```json theme={null}
{
  "formKey": "on/condo-lease",
  "canonical": {
    "schemaVersion": 1,
    "formKey": "on/condo-lease",
    "lease": {
      "tenants": [],
      "landlords": [],
      "property": { "address": { "street": "88 Harbour St", "city": "Toronto", "province": "ON" } },
      "term": { "commencement": { "year": 2026, "month": 8, "day": 1 } },
      "monthlyRent": { "cents": 320000 }
    },
    "provenance": { "aiDrafted": true }
  }
}
```

**Response (failure, 422):**

```json theme={null}
{
  "error": "draft_failed",
  "reason": "invalid_draft",
  "issues": [{ "message": "Required", "path": ["formKey"] }]
}
```

| `reason`        | Meaning                                                            |
| --------------- | ------------------------------------------------------------------ |
| `no_json`       | The model did not return parseable JSON.                           |
| `invalid_draft` | The JSON did not pass Zod validation against the canonical schema. |
| `error`         | Unexpected error (e.g. AI gateway unavailable).                    |

***

## E-signature

### `POST /api/documents/:id/signature`

Create and send a signature envelope for a generated document. The document must be in `GENERATED` status. Signing parties are derived automatically from the document's canonical data (for typed OREA packs); you may adjust them in the request.

Returns per-signer signing URLs. The raw tokens appear only in the `url` fields of the response—they are never stored or logged. Deliver them to the signers; do not log them yourself.

**Path parameters:**

| Param | Description      |
| ----- | ---------------- |
| `id`  | The document ID. |

**Request body:**

```json theme={null}
{
  "mode": "sequential",
  "expiresInDays": 14
}
```

| Field           | Type                           | Default      | Description                                                                                                                   |
| --------------- | ------------------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `mode`          | `"parallel"` or `"sequential"` | `"parallel"` | Parallel sends links to all signers at once. Sequential requires each signer to complete before the next receives their link. |
| `expiresInDays` | integer                        | none         | Days until signing links expire. Omit for no expiry.                                                                          |

**Response (201):**

```json theme={null}
{
  "envelopeId": "env_abc123",
  "links": [
    {
      "signerId": "signer_def456",
      "name": "Jane Smith",
      "email": "jane@example.com",
      "role": "buyer",
      "url": "https://app.winnerr.ai/sign/<token>"
    },
    {
      "signerId": "signer_ghi789",
      "name": "Bob Jones",
      "email": "bob@example.com",
      "role": "seller",
      "url": "https://app.winnerr.ai/sign/<token>"
    }
  ]
}
```

**Error (409 `cannot_send_for_signature`):**

Returned when the document is not in `GENERATED` status, has no signing parties, or is not a typed OREA pack.

```json theme={null}
{
  "error": "cannot_send_for_signature",
  "message": "not an engine document (no formKey)"
}
```

### `GET /api/documents/:id/signatures`

List all signature envelopes for a document. Never exposes token hashes.

**Response:**

```json theme={null}
{
  "envelopes": [
    {
      "id": "env_abc123",
      "status": "partially_signed",
      "mode": "sequential",
      "createdAt": "2026-06-25T10:00:00.000Z",
      "completedAt": null,
      "signers": [
        {
          "id": "signer_def456",
          "name": "Jane Smith",
          "email": "jane@example.com",
          "role": "buyer",
          "status": "signed",
          "signedAt": "2026-06-25T11:30:00.000Z"
        },
        {
          "id": "signer_ghi789",
          "name": "Bob Jones",
          "email": "bob@example.com",
          "role": "seller",
          "status": "sent",
          "sentAt": "2026-06-25T11:31:00.000Z"
        }
      ]
    }
  ]
}
```

### `GET /api/signatures/:envelopeId`

Fetch the current status of a specific envelope, including per-signer status.

**Path parameters:**

| Param        | Description                                                        |
| ------------ | ------------------------------------------------------------------ |
| `envelopeId` | The envelope ID returned from `POST /api/documents/:id/signature`. |

**Response:** Same shape as a single item from the `envelopes` array above.

**Error (404):** Returned when the envelope does not exist in the caller's organization.

### `DELETE /api/signatures/:envelopeId`

Void a signature envelope. Records a `voided` event with the given reason.

**Query parameters:**

| Param    | Default             | Description                         |
| -------- | ------------------- | ----------------------------------- |
| `reason` | `"voided by agent"` | Human-readable reason for the void. |

**Response:**

```json theme={null}
{ "voided": true }
```

**Error (409 `cannot_void`):** Returned when the envelope is already in a terminal status (`completed`, `declined`, `voided`, `expired`).

***

## Public signer endpoint

### `GET /api/public/sign/:token`

Open the document for the signer identified by the token. Records an `opened` event. Returns the signer's view of the document if the token is valid and non-terminal.

**Rate limit:** 60 requests per IP per minute.

**Response (active):**

```json theme={null}
{
  "state": "active",
  "signer": {
    "name": "Jane Smith",
    "role": "buyer",
    "status": "opened",
    "documentId": "doc_..."
  }
}
```

**Response (404, any bad/terminal/expired token):**

```json theme={null}
{ "state": "gone" }
```

A uniform `gone` response is returned for unknown tokens, expired links, already-signed links, and voided envelopes—no information is disclosed about the reason.

### `POST /api/public/sign/:token`

Advance the signer through the signing flow. The `action` field controls which step is executed.

**Rate limit:** 30 requests per IP per minute.

**Request body:**

```json theme={null}
{
  "action": "consent"
}
```

| Field            | Type    | Required              | Description                                                                               |
| ---------------- | ------- | --------------------- | ----------------------------------------------------------------------------------------- |
| `action`         | string  | Yes                   | One of `authenticate`, `consent`, `sign`, `decline`.                                      |
| `codeVerified`   | boolean | For `authenticate`    | Whether the caller has verified the auth code (SMS OTP or KBA).                           |
| `reason`         | string  | For `decline`         | Optional decline reason (treated as data, never executed).                                |
| `turnstileToken` | string  | For `sign`, `decline` | Cloudflare Turnstile challenge token. Required when `TURNSTILE_SECRET_KEY` is configured. |

**Actions:**

| Action         | What it does                                                                                               | Required prior state |
| -------------- | ---------------------------------------------------------------------------------------------------------- | -------------------- |
| `authenticate` | Records the `authenticated` event.                                                                         | `opened`             |
| `consent`      | Records the `consented` event (explicit intent to sign electronically).                                    | `authenticated`      |
| `sign`         | Records the `signed` event; if all signers are done, completes the envelope and generates the certificate. | `consented`          |
| `decline`      | Records the `declined` event with the optional reason.                                                     | Any active state     |

**Response (success):**

```json theme={null}
{ "ok": true }
```

For `sign`, also includes:

```json theme={null}
{ "ok": true, "completed": true }
```

**Error (409 `invalid_state`):** Returned when the action is out of order (e.g. `sign` without prior `consent`).

```json theme={null}
{ "error": "invalid_state", "message": "consent required before signing" }
```

**Response (404):** Uniform `{ "state": "gone" }` for unknown/expired/terminal tokens.

***

## Obligations

### `GET /api/documents/obligations`

Return upcoming and overdue obligations across the organization, soonest first. Org-scoped.

**Query parameters:**

| Param        | Default | Description                               |
| ------------ | ------- | ----------------------------------------- |
| `withinDays` | `30`    | Look-ahead window in days. Maximum `365`. |

**Response:**

```json theme={null}
{
  "obligations": [
    {
      "id": "obl_abc123",
      "documentId": "doc_def456",
      "dealId": "deal_ghi789",
      "type": "condition_waiver",
      "description": "Financing condition waiver",
      "dueDate": "2027-03-01T00:00:00.000Z",
      "amountCents": null,
      "urgency": "due_soon"
    },
    {
      "id": "obl_jkl012",
      "documentId": "doc_mno345",
      "dealId": null,
      "type": "closing",
      "description": null,
      "dueDate": "2027-03-15T00:00:00.000Z",
      "amountCents": null,
      "urgency": "pending"
    }
  ]
}
```

| Field         | Type            | Description                                                           |
| ------------- | --------------- | --------------------------------------------------------------------- |
| `type`        | string          | `closing`, `condition_waiver`, `deposit_due`, `expiry`, or `renewal`. |
| `urgency`     | string          | `pending`, `due_soon`, or `overdue`.                                  |
| `amountCents` | integer or null | For deposit obligations, the amount in cents.                         |
| `dealId`      | string or null  | Linked deal, if any.                                                  |

***

## Semantic search

### `GET /api/documents/search`

Search the organization's documents by natural language. Returns ranked document IDs.

**Requires the AI gateway to be configured** (embedding model).

**Query parameters:**

| Param | Required | Description                    |
| ----- | -------- | ------------------------------ |
| `q`   | Yes      | Natural-language query string. |

**Response:**

```json theme={null}
{
  "hits": [
    { "documentId": "doc_abc123", "score": 0.91 },
    { "documentId": "doc_def456", "score": 0.78 }
  ]
}
```

| Field        | Type   | Description                                            |
| ------------ | ------ | ------------------------------------------------------ |
| `documentId` | string | The matched document's ID.                             |
| `score`      | number | Cosine similarity score (0–1). Higher is more similar. |

An empty `q` returns `{ "hits": [] }` without calling the AI gateway. Without the gateway configured, all queries return an empty hits array.

***

## Clickwrap (Click)

### `POST /api/clickwrap`

Publish (upsert) a clickwrap agreement. If the `body` text has not changed since the last call with the same `key`, the call is idempotent. Changing the `body` creates a new version (new `versionHash`).

**Request body:**

```json theme={null}
{
  "key": "buyer-rep-acknowledgment",
  "title": "Buyer Representation Acknowledgment",
  "body": "By clicking Accept, you confirm that you have received and read the Buyer Representation Agreement dated [date] ..."
}
```

| Field   | Type   | Required | Description                                                       |
| ------- | ------ | -------- | ----------------------------------------------------------------- |
| `key`   | string | Yes      | Stable identifier for this agreement within your organization.    |
| `title` | string | Yes      | Display title shown on the consent page.                          |
| `body`  | string | Yes      | Full agreement text. SHA-256 hashed to produce the `versionHash`. |

**Response (201):**

```json theme={null}
{
  "id": "agr_abc123",
  "versionHash": "e3b0c44298fc..."
}
```

Embed the `id` in the consent widget URL: `https://app.winnerr.ai/consent/<id>`.

### `GET /api/clickwrap/:agreementId/acceptances`

List acceptance evidence for an agreement. Returns the most recent 200 acceptances, newest first. Org-scoped.

**Path parameters:**

| Param         | Description                                  |
| ------------- | -------------------------------------------- |
| `agreementId` | The agreement ID from `POST /api/clickwrap`. |

**Response:**

```json theme={null}
{
  "acceptances": [
    {
      "id": "acc_xyz789",
      "versionHash": "e3b0c44298fc...",
      "subjectRef": "person_def456",
      "acceptedAt": "2026-06-25T14:22:00.000Z"
    }
  ]
}
```

| Field         | Type           | Description                                                |
| ------------- | -------------- | ---------------------------------------------------------- |
| `versionHash` | string         | The exact version of the agreement text that was accepted. |
| `subjectRef`  | string or null | Optional caller-supplied identifier (person ID, email).    |
| `acceptedAt`  | string         | ISO 8601 timestamp of the acceptance (immutable).          |

***

## Public clickwrap endpoints

### `GET /api/public/clickwrap/:agreementId`

Fetch the agreement text for the consent widget. Used by the public `/consent/:agreementId` page.

**Rate limit:** 120 requests per IP per minute.

**Response:**

```json theme={null}
{
  "agreement": {
    "id": "agr_abc123",
    "key": "buyer-rep-acknowledgment",
    "title": "Buyer Representation Acknowledgment",
    "body": "By clicking Accept, you confirm ...",
    "versionHash": "e3b0c44298fc..."
  }
}
```

**Response (404):** `{ "state": "gone" }` — agreement does not exist or is not active.

### `POST /api/public/clickwrap/:agreementId`

Record an immutable acceptance. Body is optional.

**Rate limit:** 60 requests per IP per minute.

**Request body (optional):**

```json theme={null}
{
  "subjectRef": "person_def456"
}
```

| Field        | Type   | Required | Description                                                                                                                |
| ------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `subjectRef` | string | No       | Optional identifier linking this acceptance to a person or lead in your CRM (person ID, email, or other stable reference). |

**Response:**

```json theme={null}
{ "accepted": true }
```

**Response (404):** `{ "state": "gone" }` — agreement does not exist or is not active.

***

## Events emitted

The Document OS routes emit the following events on the internal event bus:

| Event             | Fired when                                                                                                                                                                                           |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DOCUMENT_SIGNED` | All signers on an envelope have signed (envelope reaches `completed`). Downstream today: obligation re-sync only. Deal-stage advance and closing-checklist creation are not yet wired to this event. |

***

## Related

* [Document OS overview](/documents/overview)
* [Generate](/documents/generate)
* [Sign](/documents/sign)
* [Locker](/documents/locker)
* [Click](/documents/click)
* [API Introduction](/api-reference/introduction)
