{
  "openapi": "3.1.0",
  "info": {
    "title": "Clause Desk API",
    "version": "1.0.0",
    "summary": "Page-cited answers about contracts, with upload, compare, export and eval.",
    "description": "The routes behind Clause Desk, a demonstration for the knowledge team of a fictional law firm. Seeded contracts are public. An upload is private to the visitor's signed `cd_anon` cookie and is deleted after 24 hours; another visitor gets 404 for it, exactly as for a document that does not exist. Every error has the same JSON shape, and every response carries an `X-Request-Id` that matches the server log. There are no accounts and no keys."
  },
  "servers": [
    {
      "url": "https://clauses.phoenixcodes.org",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Documents",
      "description": "The seeded contracts and the visitor's own uploads."
    },
    {
      "name": "Ask",
      "description": "Questions answered from one document, with citations."
    },
    {
      "name": "Upload",
      "description": "Private, temporary PDFs."
    },
    {
      "name": "Compare",
      "description": "Two versions of a document, clause by clause."
    },
    {
      "name": "Export",
      "description": "A question session as Markdown or PDF."
    },
    {
      "name": "Eval",
      "description": "Stored scorecards from `pnpm eval`."
    },
    {
      "name": "Operations",
      "description": "Called by the platform, not by visitors."
    }
  ],
  "paths": {
    "/api/documents": {
      "get": {
        "operationId": "listDocuments",
        "summary": "List documents",
        "description": "The seeded contracts, plus the caller's own uploads when the request carries a valid `cd_anon` cookie. The answer depends on the cookie, so it is never cached.",
        "tags": [
          "Documents"
        ],
        "responses": {
          "200": {
            "description": "The documents the caller may read.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "documents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentSummary"
                      }
                    }
                  },
                  "required": [
                    "documents"
                  ]
                },
                "example": {
                  "documents": [
                    {
                      "id": "mutual-nda",
                      "title": "Mutual Non-Disclosure Agreement",
                      "docType": "Non-disclosure agreement",
                      "parties": [
                        "Marlowe & Finch LLP",
                        "Orchard Logistics Ltd"
                      ],
                      "summary": "First draft of a mutual NDA.",
                      "versionGroup": "mutual-nda",
                      "version": 1,
                      "pageCount": 9,
                      "kind": "seed",
                      "expiresAt": null
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/api/documents/{id}/pdf": {
      "get": {
        "operationId": "getDocumentPdf",
        "summary": "Get a document's PDF",
        "description": "The PDF bytes, inline. A seeded contract is cached for an hour; an upload is `private, no-store` and only its owner can read it.",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A document id: a slug for a seeded contract (`mutual-nda`) or `u-` and a short random id for an upload.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
              "example": "msa"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The PDF.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/api/documents/{id}/geometry/{page}": {
      "get": {
        "operationId": "getPageGeometry",
        "summary": "Get a page's text and line boxes",
        "description": "The text of one page and the box of every line, in PDF points with the origin at the top-left. A character range from a citation becomes highlight rectangles with these. Offsets are UTF-16 offsets into `text`.",
        "tags": [
          "Documents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A document id: a slug for a seeded contract (`mutual-nda`) or `u-` and a short random id for an upload.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
              "example": "msa"
            }
          },
          {
            "name": "page",
            "in": "path",
            "required": true,
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageGeometry"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/api/ask": {
      "post": {
        "operationId": "askQuestion",
        "summary": "Ask a question about a document",
        "description": "Retrieves clauses from the one document named in the request, and streams an answer that cites them as `[p.9 §11.2]`. The reply is a server-sent stream in the AI SDK UI message format. Below the confidence threshold no model is called and the stream carries the single sentence `Not addressed in this document.` with the closest clauses in `data-meta.related`.\n\nThe stream's parts: `start`; `data-meta` (confidence first, and again at the end with the model, tokens, cost and timings); `data-passages` (every passage the model was shown, numbered, with the character ranges a viewer highlights); `text-start`, `text-delta` and `text-end` for the answer; `error`; `finish`. A failure after the first token ends the stream with an `error` part and never splices a second model's answer onto the first.\n\nEach client gets 15 questions per 10 minutes, and the whole demo has a daily token budget that resets at 00:00 UTC.",
        "tags": [
          "Ask"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AskRequest"
              },
              "example": {
                "documentId": "msa",
                "question": "What is the cap on each party's liability in a contract year?"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer, streamed.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "description": "Questions allowed per window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Questions left in this window.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Server-sent events, one JSON part per `data:` line."
                },
                "example": "data: {\"type\":\"start\",\"messageId\":\"6c1f…\"}\n\ndata: {\"type\":\"data-meta\",\"id\":\"meta\",\"data\":{\"sessionId\":\"…\",\"confidence\":0.93,\"threshold\":0.5,\"gateDeclined\":false,\"declined\":false}}\n\ndata: {\"type\":\"data-passages\",\"id\":\"passages\",\"data\":[{\"n\":1,\"label\":\"p.9 §11.2\",\"pageStart\":9,\"pageEnd\":9,\"clauseId\":\"11.2\"}]}\n\ndata: {\"type\":\"text-start\",\"id\":\"answer\"}\n\ndata: {\"type\":\"text-delta\",\"id\":\"answer\",\"delta\":\"The cap is the greater of £2,000,000 and 150% of the Charges \"}\n\ndata: {\"type\":\"text-delta\",\"id\":\"answer\",\"delta\":\"paid in that Contract Year [p.9 §11.2].\"}\n\ndata: {\"type\":\"text-end\",\"id\":\"answer\"}\n\ndata: {\"type\":\"data-meta\",\"id\":\"meta\",\"data\":{\"declined\":false,\"model\":\"llama-3.3-70b-instruct-fp8-fast\",\"ttftMs\":820,\"latencyMs\":2400}}\n\ndata: {\"type\":\"finish\"}\n\ndata: [DONE]"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/upload": {
      "post": {
        "operationId": "uploadDocument",
        "summary": "Upload a PDF",
        "description": "Takes a text PDF of at most 10 MB and 80 pages. The platform caps a request body at 4.5 MB, so the file is sent in parts of at most 4 MB (up to 3), each as the raw body of one request, tied together by `x-upload-id`. Every part but the last answers 202. The last answers 200 with a stream of newline-delimited JSON progress events, ending in `done` with the new document, or `error`.\n\nThe upload is private to the caller's `cd_anon` cookie (set on this response if the caller has none) and deleted after 24 hours. Scanned PDFs, password-protected PDFs and damaged files are refused with a reason; there is no OCR.",
        "tags": [
          "Upload"
        ],
        "parameters": [
          {
            "name": "x-upload-part",
            "in": "header",
            "description": "Which part this is, as `2/3`. Omit for a file sent in one request.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{1,2}/\\d{1,2}$",
              "example": "1/3"
            }
          },
          {
            "name": "x-upload-id",
            "in": "header",
            "description": "Ties the parts together. Required when there is more than one part.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9-]{8,64}$",
              "example": "7f3a9c1e-5b2d"
            }
          },
          {
            "name": "x-upload-size",
            "in": "header",
            "description": "The whole file's size in bytes. Required when there is more than one part.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10485760,
              "example": 9200000
            }
          },
          {
            "name": "x-upload-name",
            "in": "header",
            "description": "The file's name, used for the title when the PDF has none.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/pdf": {
              "schema": {
                "type": "string",
                "contentMediaType": "application/pdf",
                "description": "The raw bytes of one part, at most 4,000,000."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Progress events, one JSON object per line, ending in `done` or `error`.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "$ref": "#/components/schemas/UploadEvent"
                },
                "example": "{\"stage\":\"received\",\"progress\":0.08}\n{\"stage\":\"indexing\",\"progress\":0.3,\"done\":0,\"total\":24}\n{\"stage\":\"done\",\"progress\":1,\"document\":{\"id\":\"u-k3j9x2m1q8\",\"title\":\"Lease\",\"kind\":\"upload\"}}"
              }
            }
          },
          "202": {
            "description": "A part before the last was stored.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "uploadId": {
                      "type": "string"
                    },
                    "part": {
                      "type": "integer"
                    },
                    "parts": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "uploadId",
                    "part",
                    "parts"
                  ]
                },
                "example": {
                  "uploadId": "7f3a9c1e-5b2d",
                  "part": 1,
                  "parts": 3
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMedia"
          },
          "422": {
            "$ref": "#/components/responses/NotAPdf"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/compare": {
      "get": {
        "operationId": "compareDocuments",
        "summary": "Compare two documents",
        "description": "Aligns the clauses of two documents by text, heading and number, so a renumbered or dropped article still pairs, and returns the word-level diff of each pair, the list of changes and any saved one-line summaries. The result is rebuilt on every request; only the model-written summaries are stored. Both documents must be readable by the caller.",
        "tags": [
          "Compare"
        ],
        "parameters": [
          {
            "name": "left",
            "in": "query",
            "required": true,
            "description": "The earlier document's id.",
            "schema": {
              "type": "string",
              "example": "mutual-nda"
            }
          },
          {
            "name": "right",
            "in": "query",
            "required": true,
            "description": "The later document's id. Must differ from `left`.",
            "schema": {
              "type": "string",
              "example": "mutual-nda-v2"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The comparison.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Comparison"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/compare/gloss": {
      "post": {
        "operationId": "glossComparison",
        "summary": "Write the one-line summaries of a comparison",
        "description": "Has a model write one sentence per change, once, and saves them for the pair. A sentence that mentions a number the change does not contain is dropped. Nothing is charged to the rate limit or the budget when the summaries already exist or no model is configured. 8 summary requests per 10 minutes per client.",
        "tags": [
          "Compare"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompareRequest"
              },
              "example": {
                "left": "mutual-nda",
                "right": "mutual-nda-v2"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The comparison with its summaries, and how many were newly written.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Comparison"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/sessions/{id}/export": {
      "get": {
        "operationId": "exportSession",
        "summary": "Export a question session",
        "description": "The questions and answers of one session with every citation kept: Markdown with GitHub footnotes, or an A4 PDF typeset in the site's type and colours with the citations as footnotes. Only the visitor who asked the questions can export them; anyone else gets 404. 10 exports per 10 minutes per client.",
        "tags": [
          "Export"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The session id, from `data-meta.sessionId` in an ask stream.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "The file format.",
            "schema": {
              "type": "string",
              "enum": [
                "pdf",
                "md"
              ],
              "default": "pdf"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The export, as an attachment. Never cached.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Content-Disposition": {
                "description": "The suggested file name.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/eval/runs": {
      "get": {
        "operationId": "listEvalRuns",
        "summary": "List eval runs",
        "description": "Stored scorecards from `pnpm eval`, newest first, without the per-question results. A run with `retrievalOnly: true` measured retrieval and the confidence gate but had no answer model, so its answer figures are `null` rather than zero.",
        "tags": [
          "Eval"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "How many runs to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The runs.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "runs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EvalRun"
                      }
                    }
                  },
                  "required": [
                    "runs"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/eval/runs/{id}": {
      "get": {
        "operationId": "getEvalRun",
        "summary": "Get one eval run",
        "description": "One stored run with the result of every question.",
        "tags": [
          "Eval"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The run id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The run and its questions.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvalRunDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/cron/purge": {
      "get": {
        "operationId": "purgeUploads",
        "summary": "Delete expired uploads",
        "description": "Called once a day by the platform's scheduler, which sends `Authorization: Bearer <CRON_SECRET>`. Deletes uploads past their 24 hours and abandoned upload parts. Without the secret the answer is 401. Reads of an expired upload are refused whether or not this has run; the sweep reclaims space.",
        "tags": [
          "Operations"
        ],
        "security": [
          {
            "cronSecret": []
          }
        ],
        "responses": {
          "200": {
            "description": "How many uploads were removed.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "removed": {
                      "type": "integer"
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "removed",
                    "at"
                  ]
                },
                "example": {
                  "removed": 2,
                  "at": "2026-10-23T03:00:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cronSecret": {
        "type": "http",
        "scheme": "bearer",
        "description": "The `CRON_SECRET` environment variable. The platform's scheduler sends it; no visitor has it."
      }
    },
    "headers": {
      "RequestId": {
        "description": "Identifies this request in the server logs. Send your own `X-Request-Id` (6 to 64 letters, digits, dots, dashes or underscores) to correlate, or read the one returned.",
        "schema": {
          "type": "string",
          "example": "req_k3j9x0a1b2c4"
        }
      },
      "RetryAfter": {
        "description": "Seconds until the next request can succeed.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "The request is not valid. `message` says which field and why.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The bearer secret is missing or wrong.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "A browser sent the request from another site (`forbidden`). Only this site's own pages may spend the model budget; curl and servers, which send no `Origin`, are not affected.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "There is no such document, page, session or run, or it belongs to another visitor. The two cases look the same on purpose.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "A part or the whole file is over the limit (`payload_too_large`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnsupportedMedia": {
        "description": "The body is not sent as `application/pdf` (`unsupported_media_type`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotAPdf": {
        "description": "The first bytes are not a PDF's (`not_a_pdf`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests from this client (`rate_limited`), or too many live uploads (`too_many_uploads`). Wait `retryAfterSeconds`.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "A dependency (search, a model or the database) is unavailable (`upstream_unavailable`), or the daily token budget is used up (`budget_exhausted`, with `resetsAt`).",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Internal": {
        "description": "Something failed on the server (`internal`). Quote the request id.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error body has this shape.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "invalid_id",
                  "not_found",
                  "unauthorized",
                  "forbidden",
                  "rate_limited",
                  "budget_exhausted",
                  "upstream_unavailable",
                  "payload_too_large",
                  "unsupported_media_type",
                  "not_a_pdf",
                  "scanned_pdf",
                  "encrypted_pdf",
                  "too_many_pages",
                  "too_many_uploads",
                  "internal"
                ]
              },
              "message": {
                "type": "string",
                "description": "Safe to show to a person."
              },
              "requestId": {
                "type": "string"
              },
              "retryAfterSeconds": {
                "type": "integer",
                "description": "`rate_limited` and `budget_exhausted`: seconds until a request can succeed."
              },
              "resetsAt": {
                "type": "string",
                "format": "date-time",
                "description": "`budget_exhausted`: when the daily budget resets (00:00 UTC)."
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "DocumentSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "docType": {
            "type": "string"
          },
          "parties": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "summary": {
            "type": "string"
          },
          "versionGroup": {
            "type": [
              "string",
              "null"
            ],
            "description": "Documents sharing a group are versions of one another."
          },
          "version": {
            "type": "integer"
          },
          "pageCount": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "seed",
              "upload"
            ],
            "description": "`seed` contracts are public; `upload` ones are private and expire."
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When an upload is deleted; null for seeded contracts."
          }
        },
        "required": [
          "id",
          "title",
          "docType",
          "parties",
          "summary",
          "versionGroup",
          "version",
          "pageCount",
          "kind",
          "expiresAt"
        ]
      },
      "PageGeometry": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 1
          },
          "width": {
            "type": "number",
            "description": "Points."
          },
          "height": {
            "type": "number",
            "description": "Points."
          },
          "text": {
            "type": "string",
            "description": "The page's lines joined with a newline."
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "start": {
                  "type": "integer",
                  "description": "Offset of the line in `text`."
                },
                "end": {
                  "type": "integer",
                  "description": "Exclusive end offset."
                },
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                },
                "w": {
                  "type": "number"
                },
                "h": {
                  "type": "number"
                },
                "furniture": {
                  "type": "boolean",
                  "description": "A running header, footer or page number."
                },
                "segments": {
                  "type": "array",
                  "description": "The text items inside the line, between which x is interpolated.",
                  "items": {
                    "type": "object",
                    "properties": {
                      "start": {
                        "type": "integer"
                      },
                      "end": {
                        "type": "integer"
                      },
                      "x": {
                        "type": "number"
                      },
                      "w": {
                        "type": "number"
                      }
                    },
                    "required": [
                      "start",
                      "end",
                      "x",
                      "w"
                    ]
                  }
                }
              },
              "required": [
                "start",
                "end",
                "x",
                "y",
                "w",
                "h",
                "furniture",
                "segments"
              ]
            }
          }
        },
        "required": [
          "page",
          "width",
          "height",
          "text",
          "lines"
        ]
      },
      "AskRequest": {
        "type": "object",
        "properties": {
          "documentId": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,63}$",
            "description": "The one document to answer from."
          },
          "question": {
            "type": "string",
            "minLength": 3,
            "maxLength": 500,
            "description": "The question. Whitespace at either end is trimmed before the length is checked."
          },
          "sessionId": {
            "type": "string",
            "format": "uuid",
            "description": "Continue a session. Omit to start one; the id comes back in `data-meta`."
          }
        },
        "required": [
          "documentId",
          "question"
        ]
      },
      "AskMeta": {
        "type": "object",
        "description": "The `data-meta` part of an ask stream. Sent first (confidence, before any model call) and again at the end with the outcome.",
        "properties": {
          "sessionId": {
            "type": "string",
            "format": "uuid"
          },
          "questionId": {
            "type": "string",
            "format": "uuid"
          },
          "answerId": {
            "type": "string",
            "format": "uuid"
          },
          "documentId": {
            "type": "string"
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "threshold": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "gateDeclined": {
            "type": "boolean",
            "description": "The confidence gate stopped the request before any model was called."
          },
          "declined": {
            "type": "boolean",
            "description": "The reply is `Not addressed in this document.` (gate or model)."
          },
          "related": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "With a decline: the numbers (`Passage.n`) of the closest clauses."
          },
          "provider": {
            "type": "string",
            "enum": [
              "anthropic",
              "workers-ai"
            ]
          },
          "model": {
            "type": "string"
          },
          "promptTokens": {
            "type": "integer"
          },
          "completionTokens": {
            "type": "integer"
          },
          "costUsd": {
            "type": "number",
            "description": "0 on Workers AI."
          },
          "ttftMs": {
            "type": "integer",
            "description": "Milliseconds to the first streamed token."
          },
          "latencyMs": {
            "type": "integer"
          }
        },
        "required": [
          "sessionId",
          "questionId",
          "answerId",
          "documentId",
          "confidence",
          "threshold",
          "gateDeclined",
          "declined"
        ]
      },
      "Passage": {
        "type": "object",
        "description": "The `data-passages` part: one retrieved clause chunk, numbered in rank order.",
        "properties": {
          "n": {
            "type": "integer",
            "minimum": 1
          },
          "chunkId": {
            "type": "string"
          },
          "documentId": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "description": "The reference the answer writes between brackets, e.g. `p.9 §11.2`."
          },
          "pageStart": {
            "type": "integer"
          },
          "pageEnd": {
            "type": "integer"
          },
          "clauseId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Primary clause number of the chunk."
          },
          "clauseIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "heading": {
            "type": "string"
          },
          "breadcrumb": {
            "type": "string"
          },
          "ranges": {
            "type": "array",
            "description": "The character ranges of the clause's own text, per page.",
            "items": {
              "$ref": "#/components/schemas/TextRange"
            }
          },
          "excerpt": {
            "type": "string"
          }
        },
        "required": [
          "n",
          "chunkId",
          "documentId",
          "label",
          "pageStart",
          "pageEnd",
          "clauseId",
          "clauseIds",
          "heading",
          "breadcrumb",
          "ranges",
          "excerpt"
        ]
      },
      "TextRange": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer"
          },
          "start": {
            "type": "integer"
          },
          "end": {
            "type": "integer"
          }
        },
        "required": [
          "page",
          "start",
          "end"
        ]
      },
      "UploadEvent": {
        "type": "object",
        "description": "One line of the upload's progress stream. `stage` runs received, reading, indexing, saving, then done or error; `progress` only goes up.",
        "properties": {
          "stage": {
            "type": "string",
            "enum": [
              "received",
              "reading",
              "indexing",
              "saving",
              "done",
              "error"
            ]
          },
          "progress": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "done": {
            "type": "integer",
            "description": "`indexing`: chunks embedded so far."
          },
          "total": {
            "type": "integer",
            "description": "`indexing`: chunks to embed."
          },
          "document": {
            "$ref": "#/components/schemas/DocumentSummary",
            "description": "`done`: the new document."
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "invalid_id",
                  "not_found",
                  "unauthorized",
                  "forbidden",
                  "rate_limited",
                  "budget_exhausted",
                  "upstream_unavailable",
                  "payload_too_large",
                  "unsupported_media_type",
                  "not_a_pdf",
                  "scanned_pdf",
                  "encrypted_pdf",
                  "too_many_pages",
                  "too_many_uploads",
                  "internal"
                ]
              },
              "message": {
                "type": "string",
                "description": "Safe to show to a person."
              },
              "requestId": {
                "type": "string"
              },
              "retryAfterSeconds": {
                "type": "integer",
                "description": "`rate_limited` and `budget_exhausted`: seconds until a request can succeed."
              },
              "resetsAt": {
                "type": "string",
                "format": "date-time",
                "description": "`budget_exhausted`: when the daily budget resets (00:00 UTC)."
              }
            },
            "required": [
              "code",
              "message"
            ],
            "description": "`error`: why it stopped."
          }
        },
        "required": [
          "stage"
        ]
      },
      "CompareRequest": {
        "type": "object",
        "properties": {
          "left": {
            "type": "string",
            "description": "The earlier document's id."
          },
          "right": {
            "type": "string",
            "description": "The later document's id."
          }
        },
        "required": [
          "left",
          "right"
        ]
      },
      "Comparison": {
        "type": "object",
        "properties": {
          "left": {
            "$ref": "#/components/schemas/DocumentSummary"
          },
          "right": {
            "$ref": "#/components/schemas/DocumentSummary"
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompareRow"
            }
          },
          "changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClauseChange"
            }
          },
          "stats": {
            "$ref": "#/components/schemas/CompareStats"
          },
          "gloss": {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "ready",
                  "pending",
                  "unavailable"
                ],
                "description": "`ready`: every change has a summary. `pending`: a model can write them (POST /api/compare/gloss). `unavailable`: no model is configured."
              },
              "model": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The model that wrote the summaries."
              }
            },
            "required": [
              "state",
              "model"
            ]
          },
          "written": {
            "type": "integer",
            "description": "POST only: summaries newly written by this call."
          }
        },
        "required": [
          "left",
          "right",
          "rows",
          "changes",
          "stats",
          "gloss"
        ]
      },
      "CompareRow": {
        "type": "object",
        "description": "One aligned clause pair, or an added or removed clause.",
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable anchor within one comparison: `r0`, `r1`, …"
          },
          "kind": {
            "type": "string",
            "enum": [
              "same",
              "added",
              "removed",
              "changed"
            ]
          },
          "leftLabel": {
            "type": [
              "string",
              "null"
            ]
          },
          "rightLabel": {
            "type": [
              "string",
              "null"
            ]
          },
          "leftClauseId": {
            "type": [
              "string",
              "null"
            ]
          },
          "rightClauseId": {
            "type": [
              "string",
              "null"
            ]
          },
          "heading": {
            "type": "string"
          },
          "article": {
            "type": "string"
          },
          "leftPage": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rightPage": {
            "type": [
              "integer",
              "null"
            ]
          },
          "left": {
            "type": "array",
            "description": "The earlier version's words: `same` and `del` segments.",
            "items": {
              "$ref": "#/components/schemas/DiffSegment"
            }
          },
          "right": {
            "type": "array",
            "description": "The later version's words: `same` and `ins` segments.",
            "items": {
              "$ref": "#/components/schemas/DiffSegment"
            }
          },
          "renumbered": {
            "type": "boolean",
            "description": "The clause has a different number in each version. A renumbering alone is not a change."
          },
          "score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Match confidence of the pair."
          }
        },
        "required": [
          "key",
          "kind",
          "leftLabel",
          "rightLabel",
          "leftClauseId",
          "rightClauseId",
          "heading",
          "article",
          "leftPage",
          "rightPage",
          "left",
          "right",
          "renumbered",
          "score"
        ]
      },
      "DiffSegment": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "same",
              "ins",
              "del"
            ]
          },
          "text": {
            "type": "string"
          }
        },
        "required": [
          "kind",
          "text"
        ]
      },
      "ClauseChange": {
        "type": "object",
        "description": "One entry of the What changed list.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "added",
              "removed",
              "changed"
            ]
          },
          "clauseId": {
            "type": "string"
          },
          "heading": {
            "type": "string"
          },
          "leftPage": {
            "type": [
              "integer",
              "null"
            ]
          },
          "rightPage": {
            "type": [
              "integer",
              "null"
            ]
          },
          "gloss": {
            "type": "string",
            "description": "One model-written sentence, present only when a model wrote it."
          },
          "leftClauseId": {
            "type": [
              "string",
              "null"
            ]
          },
          "rightClauseId": {
            "type": [
              "string",
              "null"
            ]
          },
          "anchor": {
            "type": "string",
            "description": "The row to jump to."
          },
          "digest": {
            "type": "string",
            "description": "Short hash of the changed words; a saved summary is reused only while it matches."
          }
        },
        "required": [
          "kind",
          "clauseId",
          "heading",
          "leftPage",
          "rightPage"
        ]
      },
      "CompareStats": {
        "type": "object",
        "properties": {
          "changed": {
            "type": "integer"
          },
          "added": {
            "type": "integer"
          },
          "removed": {
            "type": "integer"
          },
          "unchanged": {
            "type": "integer"
          },
          "renumbered": {
            "type": "integer"
          }
        },
        "required": [
          "changed",
          "added",
          "removed",
          "unchanged",
          "renumbered"
        ]
      },
      "EvalRun": {
        "type": "object",
        "description": "One stored scorecard. A figure the run could not measure is `null`, never zero.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "gitSha": {
            "type": [
              "string",
              "null"
            ],
            "description": "Short commit the run was made from."
          },
          "model": {
            "type": [
              "string",
              "null"
            ],
            "description": "Model that wrote the answers; null when none ran."
          },
          "judgeModel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Model that scored faithfulness and correctness."
          },
          "retrievalOnly": {
            "type": "boolean",
            "description": "Only retrieval and the confidence gate ran: no answer model."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "What a reader needs to interpret the numbers, e.g. how the embeddings were made."
          },
          "confidenceThreshold": {
            "type": [
              "number",
              "null"
            ]
          },
          "promptVersion": {
            "type": [
              "string",
              "null"
            ]
          },
          "judgePromptVersion": {
            "type": [
              "string",
              "null"
            ]
          },
          "questionCount": {
            "type": "integer"
          },
          "outOfScopeCount": {
            "type": "integer"
          },
          "pageHitAt1": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "pageHitAt3": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "clauseHitAt1": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "clauseHitAt3": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "mrr": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Mean reciprocal rank."
          },
          "declineAccuracy": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Out-of-scope questions that ended in a decline."
          },
          "gateDeclineAccuracy": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Out-of-scope questions the gate stopped before any model call."
          },
          "falseDeclineRate": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Answerable questions declined anyway."
          },
          "faithfulness": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 1,
            "maximum": 5,
            "description": "Mean judge score: is every claim supported by the passages."
          },
          "correctness": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 1,
            "maximum": 5,
            "description": "Mean judge score against the reference answer."
          },
          "citationRate": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Answers whose every citation points at a passage the model saw."
          },
          "latencyP50Ms": {
            "type": "integer"
          },
          "latencyP95Ms": {
            "type": "integer"
          },
          "ttftP50Ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ttftP95Ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "promptTokens": {
            "type": "integer"
          },
          "completionTokens": {
            "type": "integer"
          },
          "totalTokens": {
            "type": "integer"
          },
          "costUsd": {
            "type": "number",
            "description": "Billed cost of the answers; 0 on Workers AI."
          }
        },
        "required": [
          "id",
          "createdAt",
          "gitSha",
          "model",
          "judgeModel",
          "retrievalOnly",
          "notes",
          "questionCount",
          "outOfScopeCount",
          "pageHitAt1",
          "pageHitAt3",
          "clauseHitAt1",
          "clauseHitAt3",
          "mrr",
          "declineAccuracy",
          "gateDeclineAccuracy",
          "falseDeclineRate",
          "faithfulness",
          "correctness",
          "citationRate",
          "latencyP50Ms",
          "latencyP95Ms",
          "totalTokens",
          "costUsd"
        ]
      },
      "EvalRunDetail": {
        "type": "object",
        "description": "An EvalRun with the result of every question.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EvalQuestionResult"
            }
          }
        },
        "required": [
          "id",
          "results"
        ]
      },
      "EvalQuestionResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "`g01` to `g30` (gold) or `o01` to `o10` (out of scope)."
          },
          "kind": {
            "type": "string",
            "enum": [
              "gold",
              "out_of_scope"
            ]
          },
          "question": {
            "type": "string"
          },
          "documentId": {
            "type": "string"
          },
          "expectedPage": {
            "type": [
              "integer",
              "null"
            ]
          },
          "expectedClause": {
            "type": [
              "string",
              "null"
            ]
          },
          "retrieved": {
            "type": "array",
            "description": "The passages the model was shown, in rank order.",
            "items": {
              "type": "object",
              "properties": {
                "page": {
                  "type": "integer"
                },
                "clause": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "required": [
                "page",
                "clause"
              ]
            }
          },
          "pageRank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Rank of the first passage on the expected page."
          },
          "clauseRank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Rank of the first passage with the expected clause."
          },
          "confidence": {
            "type": "number"
          },
          "declined": {
            "type": "boolean"
          },
          "gateDeclined": {
            "type": "boolean"
          },
          "pass": {
            "type": "boolean"
          },
          "answer": {
            "type": "string"
          },
          "validCitations": {
            "type": "integer"
          },
          "totalCitations": {
            "type": "integer"
          },
          "faithfulness": {
            "type": [
              "integer",
              "null"
            ]
          },
          "correctness": {
            "type": [
              "integer",
              "null"
            ]
          },
          "judgeNote": {
            "type": [
              "string",
              "null"
            ]
          },
          "latencyMs": {
            "type": "integer"
          },
          "ttftMs": {
            "type": "integer"
          },
          "promptTokens": {
            "type": "integer"
          },
          "completionTokens": {
            "type": "integer"
          },
          "costUsd": {
            "type": "number"
          },
          "provider": {
            "type": "string"
          },
          "model": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "kind",
          "question",
          "documentId",
          "expectedPage",
          "expectedClause",
          "retrieved",
          "pageRank",
          "clauseRank",
          "confidence",
          "declined",
          "gateDeclined",
          "pass",
          "answer",
          "validCitations",
          "totalCitations",
          "faithfulness",
          "correctness",
          "judgeNote",
          "latencyMs",
          "promptTokens",
          "completionTokens"
        ]
      }
    }
  }
}