Skip to main content
The Documents API is the programmatic surface behind Document OS. 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 for base URLs, bearer-token forwarding, and standard 401 behavior.

Authentication

Authenticated routes expect the session bearer token forwarded by apps/app:
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:
{
  "used": 12,
  "included": 15,
  "periodLabel": "June 2026"
}
FieldTypeDescription
usedintegerDocuments rendered (generated) this calendar month.
includedinteger or nullThe plan’s monthly allotment from DOCUMENTS_INCLUDED_PER_MONTH. null when unset (the meter still counts, but the allotment is not enforced).
periodLabelstringHuman-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:
{
  "prompt": "Draft a condo lease for 88 Harbour St unit 2502, $3,200/month, one year from August 1 2026"
}
FieldTypeRequiredDescription
promptstringYesNatural-language description of the document.
Response (success):
{
  "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):
{
  "error": "draft_failed",
  "reason": "invalid_draft",
  "issues": [{ "message": "Required", "path": ["formKey"] }]
}
reasonMeaning
no_jsonThe model did not return parseable JSON.
invalid_draftThe JSON did not pass Zod validation against the canonical schema.
errorUnexpected 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:
ParamDescription
idThe document ID.
Request body:
{
  "mode": "sequential",
  "expiresInDays": 14
}
FieldTypeDefaultDescription
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.
expiresInDaysintegernoneDays until signing links expire. Omit for no expiry.
Response (201):
{
  "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.
{
  "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:
{
  "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:
ParamDescription
envelopeIdThe 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:
ParamDefaultDescription
reason"voided by agent"Human-readable reason for the void.
Response:
{ "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):
{
  "state": "active",
  "signer": {
    "name": "Jane Smith",
    "role": "buyer",
    "status": "opened",
    "documentId": "doc_..."
  }
}
Response (404, any bad/terminal/expired token):
{ "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:
{
  "action": "consent"
}
FieldTypeRequiredDescription
actionstringYesOne of authenticate, consent, sign, decline.
codeVerifiedbooleanFor authenticateWhether the caller has verified the auth code (SMS OTP or KBA).
reasonstringFor declineOptional decline reason (treated as data, never executed).
turnstileTokenstringFor sign, declineCloudflare Turnstile challenge token. Required when TURNSTILE_SECRET_KEY is configured.
Actions:
ActionWhat it doesRequired prior state
authenticateRecords the authenticated event.opened
consentRecords the consented event (explicit intent to sign electronically).authenticated
signRecords the signed event; if all signers are done, completes the envelope and generates the certificate.consented
declineRecords the declined event with the optional reason.Any active state
Response (success):
{ "ok": true }
For sign, also includes:
{ "ok": true, "completed": true }
Error (409 invalid_state): Returned when the action is out of order (e.g. sign without prior consent).
{ "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:
ParamDefaultDescription
withinDays30Look-ahead window in days. Maximum 365.
Response:
{
  "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"
    }
  ]
}
FieldTypeDescription
typestringclosing, condition_waiver, deposit_due, expiry, or renewal.
urgencystringpending, due_soon, or overdue.
amountCentsinteger or nullFor deposit obligations, the amount in cents.
dealIdstring or nullLinked deal, if any.

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:
ParamRequiredDescription
qYesNatural-language query string.
Response:
{
  "hits": [
    { "documentId": "doc_abc123", "score": 0.91 },
    { "documentId": "doc_def456", "score": 0.78 }
  ]
}
FieldTypeDescription
documentIdstringThe matched document’s ID.
scorenumberCosine 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:
{
  "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] ..."
}
FieldTypeRequiredDescription
keystringYesStable identifier for this agreement within your organization.
titlestringYesDisplay title shown on the consent page.
bodystringYesFull agreement text. SHA-256 hashed to produce the versionHash.
Response (201):
{
  "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:
ParamDescription
agreementIdThe agreement ID from POST /api/clickwrap.
Response:
{
  "acceptances": [
    {
      "id": "acc_xyz789",
      "versionHash": "e3b0c44298fc...",
      "subjectRef": "person_def456",
      "acceptedAt": "2026-06-25T14:22:00.000Z"
    }
  ]
}
FieldTypeDescription
versionHashstringThe exact version of the agreement text that was accepted.
subjectRefstring or nullOptional caller-supplied identifier (person ID, email).
acceptedAtstringISO 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:
{
  "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):
{
  "subjectRef": "person_def456"
}
FieldTypeRequiredDescription
subjectRefstringNoOptional identifier linking this acceptance to a person or lead in your CRM (person ID, email, or other stable reference).
Response:
{ "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:
EventFired when
DOCUMENT_SIGNEDAll signers on an envelope have signed (envelope reaches completed). Downstream: obligation re-sync, deal-stage advance, closing-checklist spawn.