{
  "openapi": "3.1.1",
  "info": {
    "title": "Ferrith Chat — Programmable API",
    "description": "Tenant-scoped programmable API authenticated by `fck_` keys. Carries the workflow run surface (start, follow, cancel, input documents, rendered outputs), the drafting templates API (produce a document from a template) and the OpenAI-compatible chat completions adapter. The getting-started guide is at https://docs.ferrith.ai/reference/api/.",
    "version": "v1"
  },
  "servers": [
    {
      "url": "https://api.ferrith.ai"
    }
  ],
  "paths": {
    "/workflows": {
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "List workflows",
        "description": "Lists the workflows an API key can start: only workflows with an active version, narrowed to the key's allowlist when one is set. Drafts are never visible to key callers.",
        "operationId": "listWorkflows",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/workflows/{id}": {
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "Get a workflow",
        "description": "One workflow with its active version projected (steps, agent references). Key callers never see drafts or archived versions. A workflow the key's allowlist omits answers 403 `workflow_not_in_allowlist`; an unknown id answers 404.",
        "operationId": "getWorkflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/workflows/runs": {
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Start a workflow run",
        "description": "Enqueues a run and returns 202 immediately — follow it on the events stream or poll the run. `input` is a plain string when the workflow's entry step is `kind: agent`, or a JSON object matching the workflow's input schema when it is `kind: input` — `format: \"document\"` fields take the `{ documentId, documentVersionId }` reference returned by the workflow-inputs upload. The workflow must be on the key's allowlist.",
        "operationId": "startWorkflowRun",
        "requestBody": {
          "description": "The workflow to start and its input.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "workflowId"
                ],
                "type": "object",
                "properties": {
                  "workflowId": {
                    "type": "string",
                    "description": "The workflow's id, from `GET /workflows`."
                  },
                  "input": {
                    "description": "The run input. A workflow that starts with an **input form** takes an object matching its input schema (`GET /workflows/{id}` returns the schema; a document field takes the `{ documentId, documentVersionId }` reference object from the upload response, never a bare id). A workflow that starts with an **intake agent** takes a plain string."
                  }
                }
              },
              "example": {
                "workflowId": "1c684e4bff9243249a55d1679cb4b9df",
                "input": {
                  "clientName": "Acme Ltd",
                  "matterReference": "ACME-2026-014"
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartWorkflowRunResponse"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:run"
      },
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "List workflow runs",
        "description": "The most recent 50 runs visible to the caller after filtering — for a key, API-initiated runs only, narrowed by its allowlist. Optional filters: `workflowId`, `status`, `startedFrom`, `startedTo` (dates; the end date runs to the end of that day).",
        "operationId": "listWorkflowRuns",
        "parameters": [
          {
            "name": "workflowId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "startedFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "startedTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "initiatorUserId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/workflows/runs/{runId}/cancel": {
      "post": {
        "tags": [
          "Runs"
        ],
        "summary": "Cancel a workflow run",
        "description": "Requests cancellation of a run. An in-flight run is aborted mid-stream; any other non-terminal run (paused, queued, or stalled) is marked Cancelled directly. Returns `{ runId, requested }` — or `{ runId, status, alreadyTerminal: true }` when the run had already finished. A key can cancel only runs started with that same key.",
        "operationId": "cancelWorkflowRun",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:run"
      }
    },
    "/workflows/runs/{runId}": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "Get a workflow run",
        "description": "The full run record: status, timings, token totals, per-step outputs (decrypted server-side), and — on successful `kind: render` steps — a `renderedArtifact` block whose `downloadPath` is the ready-made download URL. Keys read API-initiated runs only.",
        "operationId": "getWorkflowRun",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/workflows/runs/{runId}/events": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "Stream run events (SSE)",
        "description": "A `text/event-stream` of the run's live events — a `connected` preamble, then the step and run lifecycle events — ending when the run reaches a terminal state. A run that is already terminal answers 409 `run_terminal`: fetch the run instead.",
        "operationId": "streamWorkflowRunEvents",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "409": {
            "description": "Conflict"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/workflows/runs/{runId}/files/{stepId}/download": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "Download a rendered output",
        "description": "Streams the PDF or DOCX a successful `kind: render` step produced, decrypted, with the display filename in `Content-Disposition` — or, with `encoding=base64`, a JSON envelope carrying the bytes. The path comes ready-made from the run record's `renderedArtifact.downloadPath`.",
        "operationId": "downloadWorkflowRunFile",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "stepId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "encoding",
            "in": "query",
            "description": "`binary` (the default) answers the file's bytes with its content type and a `Content-Disposition` filename; `base64` answers `application/json` with the file's details and its bytes base64-encoded in `data`.",
            "schema": {
              "enum": [
                "binary",
                "base64"
              ],
              "type": "string",
              "default": "binary"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file — its bytes by default, or with `encoding=base64` the JSON envelope carrying them.",
            "content": {
              "application/json": {
                "schema": {
                  "required": [
                    "filename",
                    "contentType",
                    "format",
                    "sizeBytes",
                    "data"
                  ],
                  "type": "object",
                  "properties": {
                    "filename": {
                      "type": "string"
                    },
                    "contentType": {
                      "type": "string"
                    },
                    "format": {
                      "type": "string",
                      "description": "`pdf`, `docx`, `json` or `markdown`."
                    },
                    "sizeBytes": {
                      "type": "integer",
                      "description": "The decoded length of `data`."
                    },
                    "data": {
                      "type": "string",
                      "description": "The file bytes, base64-encoded."
                    }
                  },
                  "description": "The base64 envelope: the workflow-input upload's JSON shape (`filename`, `contentType`, `data`) with the file's details beside the bytes."
                },
                "example": {
                  "filename": "engagement-letter.docx",
                  "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                  "format": "docx",
                  "sizeBytes": 48211,
                  "data": "UEsDBBQABgAI…"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/workflows/runs/{runId}/input-documents/{documentVersionId}/download": {
      "get": {
        "tags": [
          "Runs"
        ],
        "summary": "Download a run input document",
        "description": "Streams the original bytes of a document version the run consumed, with its stored content type. The version must appear on the run's own input (or knowledge-base coverage) lists — anything else reads as 404.",
        "operationId": "downloadWorkflowRunInputDocument",
        "parameters": [
          {
            "name": "runId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "documentVersionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflows:read"
      }
    },
    "/v1/chat/completions": {
      "post": {
        "tags": [
          "Chat completions"
        ],
        "summary": "Create a chat completion",
        "description": "OpenAI-compatible: the request and response are the standard `chat.completion` shapes (snake_case wire), so an OpenAI SDK pointed at `/v1` works as-is. `stream: true` switches the response to a `text/event-stream` of `chat.completion.chunk` events ending with `data: [DONE]`. Stateless — nothing is persisted. `model` must be one of the ids `GET /v1/models` returns for your workspace; any other is refused with 400 `model_not_allowed`. `tools` / `tool_choice` are refused with 400 `tools_not_supported_yet`.",
        "operationId": "createChatCompletion",
        "requestBody": {
          "description": "An OpenAI-compatible chat completion request (snake_case wire).",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "model",
                  "messages"
                ],
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "A model id from `GET /v1/models`."
                  },
                  "messages": {
                    "type": "array",
                    "items": {
                      "required": [
                        "role",
                        "content"
                      ],
                      "type": "object",
                      "properties": {
                        "role": {
                          "type": "string",
                          "description": "`system`, `user`, or `assistant`."
                        },
                        "content": {
                          "type": "string"
                        }
                      }
                    },
                    "description": "The conversation so far — the client sends the full array every turn; nothing is persisted."
                  },
                  "stream": {
                    "type": "boolean",
                    "description": "When true the response is a `text/event-stream` of `chat.completion.chunk` events ending with `data: [DONE]`."
                  },
                  "temperature": {
                    "type": "number"
                  },
                  "max_completion_tokens": {
                    "type": "integer",
                    "description": "Output-token cap. On a reasoning model it bounds the reasoning channel too — budget for both. Preferred over `max_tokens` when both are sent."
                  },
                  "max_tokens": {
                    "type": "integer",
                    "description": "The legacy name of the output-token cap; accepted as a fallback."
                  },
                  "response_format": {
                    "description": "Optional structured-output constraint, forwarded verbatim (e.g. `{ \"type\": \"json_schema\", … }`). Refused with `structured_output_unsupported` when the deployment cannot honour it."
                  }
                }
              },
              "example": {
                "model": "gpt-oss-120b",
                "messages": [
                  {
                    "role": "user",
                    "content": "Summarise the notice period in three bullet points."
                  }
                ],
                "stream": false,
                "max_completion_tokens": 512
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "chat:completions"
      }
    },
    "/v1/models": {
      "get": {
        "tags": [
          "Chat completions"
        ],
        "summary": "List models",
        "description": "OpenAI-compatible model listing (snake_case wire): the chat models your workspace allows for API access. When the workspace fixes one model for API access, only that model is listed. Use these ids in the `model` field of a completion request.",
        "operationId": "listModels",
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "chat:completions"
      }
    },
    "/workflow-inputs/documents": {
      "post": {
        "tags": [
          "Workflow inputs"
        ],
        "summary": "Upload a workflow input document",
        "description": "Stages a document (PDF, DOCX, Markdown, or plain text; up to 50 MB decoded) for a later run start. Two body shapes: `multipart/form-data` with a `file` field, or `application/json` with `{ filename, contentType, data }` (base64). Returns 202 with the `{ documentId, documentVersionId }` reference and `status: \"processing\"` — poll the per-document GET until `ready` before referencing it in a run.",
        "operationId": "uploadWorkflowInputDocument",
        "requestBody": {
          "description": "The document to stage — as JSON carrying the bytes base64-encoded (for systems that emit binaries that way), or as a multipart file.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "filename",
                  "contentType",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "filename": {
                    "type": "string"
                  },
                  "contentType": {
                    "type": "string",
                    "description": "The document's MIME type, e.g. `application/pdf`."
                  },
                  "data": {
                    "type": "string",
                    "description": "The file bytes, base64-encoded. The size cap applies to the decoded bytes."
                  }
                }
              },
              "example": {
                "filename": "contract.pdf",
                "contentType": "application/pdf",
                "data": "JVBERi0xLjcK…"
              }
            },
            "multipart/form-data": {
              "schema": {
                "required": [
                  "file"
                ],
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "description": "The document (PDF, DOCX, Markdown, or plain text).",
                    "format": "binary"
                  }
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowInputUploadResponse"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflow-inputs:write"
      }
    },
    "/workflow-inputs/documents/{documentId}": {
      "get": {
        "tags": [
          "Workflow inputs"
        ],
        "summary": "Get an uploaded document's status",
        "description": "The poll half of the upload contract: `processing` until ingestion lands, then `ready` (or `failed` with a short `processingError` code). Only the uploading key can read it; `usedByRunId` names the most recent run to use the upload (a document may be used by several runs), or null while none has.",
        "operationId": "getWorkflowInputDocument",
        "parameters": [
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowInputDocumentStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "workflow-inputs:write"
      }
    },
    "/document-templates": {
      "get": {
        "tags": [
          "Document templates"
        ],
        "summary": "List the templates this key may use",
        "description": "The active drafting templates whose document type and whose own restriction the key's issuing user can access. `requiresReview` means a person must attest to a produced document in the app before its content can be downloaded.",
        "operationId": "listDocumentTemplates",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DocumentTemplateSummaryResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "document-templates:read"
      }
    },
    "/document-templates/{templateId}": {
      "get": {
        "tags": [
          "Document templates"
        ],
        "summary": "Get a template and its input schema",
        "description": "The template with `inputSchema` — JSON Schema 2020-12, the questions the app's form asks: a field with a workspace default carries `default`, a fixed one `readOnly: true` as well (leave it out of a post), an image field is a base64 string (`contentEncoding: base64`), and `x-groups` names the form's sections. `needsAttention` means a document author has to look at the recipe before it can be used.",
        "operationId": "getDocumentTemplate",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentTemplateResponse"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "document-templates:read"
      }
    },
    "/document-templates/{templateId}/documents": {
      "post": {
        "tags": [
          "Document templates"
        ],
        "summary": "Produce a document from a template",
        "description": "Validates `input` against the template's input schema, closes the plan with the answers, assembles the document and renders its DOCX and PDF — synchronously, with no model involved. `201` with `status: assembled` when every field was answered or defaulted, `status: incomplete` (the missing fields named in `unresolvedFields`) otherwise; with `strict: true` a missing required field is `422 fields_missing` and nothing is produced. A date is `yyyy-MM-dd` (a date-time's date is taken as written); an image field takes a PNG or JPEG picture as a base64 string, a `data:` URI prefix accepted, up to 1 MB and 2,000 pixels on each side. A value posted for a fixed field is `400 field_fixed`; a value that doesn't fit is `400 input_validation_failed` with `errors[]`; a body over 10 MB is `413`. The document belongs to the key's issuing user and is listed in the app on the drafts page's API tab.",
        "operationId": "createDocumentFromTemplate",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The answers as a nested JSON document matching the template's `inputSchema`, an optional title, and whether a missing required answer refuses the whole post.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "input"
                ],
                "type": "object",
                "properties": {
                  "input": {
                    "type": "object",
                    "description": "The answers, nested as the template's `inputSchema` describes. Leave a field with a workspace default out to take the default; never post a `readOnly` (fixed) field. A date is `yyyy-MM-dd`. An image field takes a PNG or JPEG picture as a base64 string — a `data:` URI prefix is accepted — up to 1 MB and 2,000 pixels on each side."
                  },
                  "title": {
                    "type": "string",
                    "description": "The document's title in the app; the document type's name when omitted."
                  },
                  "strict": {
                    "type": "boolean",
                    "description": "When true a missing required answer is `422 fields_missing` and nothing is produced; when false the document is produced with `status: incomplete`.",
                    "default": false
                  }
                }
              },
              "example": {
                "input": {
                  "client": {
                    "full_name": "Ann Example",
                    "address": "1 High Street, Bath"
                  },
                  "matter": {
                    "reference": "AE-2026-014"
                  }
                },
                "title": "Engagement letter — Ann Example",
                "strict": true
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateDocumentResponse"
                }
              }
            }
          },
          "413": {
            "description": "Payload Too Large"
          },
          "422": {
            "description": "Unprocessable Entity"
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "documents:write"
      }
    },
    "/document-templates/{templateId}/documents/{documentId}": {
      "get": {
        "tags": [
          "Document templates"
        ],
        "summary": "Get a produced document's record",
        "description": "The document's status, its unresolved fields and issues, whether a person has reviewed it and whether a review is required before its content can be downloaded, and the links. A key reads only the documents its issuing user produced through the API.",
        "operationId": "getTemplateDocument",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateDocumentResponse"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "documents:read"
      }
    },
    "/document-templates/{templateId}/documents/{documentId}/content": {
      "get": {
        "tags": [
          "Document templates"
        ],
        "summary": "Download a produced document",
        "description": "The rendered document — `format` is `docx` (the default), `pdf` or `markdown`. The bytes by default, with the filename in `Content-Disposition`; with `encoding=base64` a JSON envelope carrying them. `403 review_required` until a person attests to the document in the app, when the template or its document type asks for a review.",
        "operationId": "downloadTemplateDocumentContent",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "encoding",
            "in": "query",
            "description": "`binary` (the default) answers the file's bytes with its content type and a `Content-Disposition` filename; `base64` answers `application/json` with the file's details and its bytes base64-encoded in `data`.",
            "schema": {
              "enum": [
                "binary",
                "base64"
              ],
              "type": "string",
              "default": "binary"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file — its bytes by default, or with `encoding=base64` the JSON envelope carrying them.",
            "content": {
              "application/json": {
                "schema": {
                  "required": [
                    "filename",
                    "contentType",
                    "format",
                    "sizeBytes",
                    "data"
                  ],
                  "type": "object",
                  "properties": {
                    "filename": {
                      "type": "string"
                    },
                    "contentType": {
                      "type": "string"
                    },
                    "format": {
                      "type": "string",
                      "description": "`pdf`, `docx`, `json` or `markdown`."
                    },
                    "sizeBytes": {
                      "type": "integer",
                      "description": "The decoded length of `data`."
                    },
                    "data": {
                      "type": "string",
                      "description": "The file bytes, base64-encoded."
                    }
                  },
                  "description": "The base64 envelope: the workflow-input upload's JSON shape (`filename`, `contentType`, `data`) with the file's details beside the bytes."
                },
                "example": {
                  "filename": "engagement-letter.docx",
                  "contentType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                  "format": "docx",
                  "sizeBytes": 48211,
                  "data": "UEsDBBQABgAI…"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing or is not a valid key (`invalid_api_key`), or the issuing user no longer belongs to a workspace (`no_tenant`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The key does not carry a required scope (`missing_scope`); the workflow is not on the key's allowlist (`workflow_not_in_allowlist`); or the feature is not enabled for this workspace (e.g. `workflows_disabled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorDetailEnvelope"
                }
              }
            }
          },
          "423": {
            "description": "The workspace's encryption key is momentarily unavailable (`tenant_locked`). Transient — retry shortly; the call succeeds again once the key store is reachable."
          },
          "429": {
            "description": "The key has exceeded its per-minute rate limit (`error.type: rate_limited`). Honour the Retry-After header before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window frees up.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InferenceErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "API keys are disabled on this deployment (`feature_disabled`).",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer challenge naming the refusal code.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-ferrith-required-scope": "documents:read"
      }
    }
  },
  "components": {
    "schemas": {
      "DocumentTemplateBlockResponse": {
        "required": [
          "id",
          "key",
          "title"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "title": {
            "type": "string"
          }
        }
      },
      "DocumentTemplateResponse": {
        "required": [
          "id",
          "name",
          "description",
          "documentType",
          "documentTypeId",
          "requiresReview",
          "needsAttention",
          "blocks",
          "inputSchema",
          "fieldKeys",
          "requiredFieldKeys",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "null",
              "string"
            ]
          },
          "documentType": {
            "type": "string"
          },
          "documentTypeId": {
            "type": "string"
          },
          "requiresReview": {
            "type": "boolean"
          },
          "needsAttention": {
            "type": "boolean"
          },
          "blocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DocumentTemplateBlockResponse"
            }
          },
          "inputSchema": {
            "$ref": "#/components/schemas/JsonElement"
          },
          "fieldKeys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiredFieldKeys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DocumentTemplateSummaryResponse": {
        "required": [
          "id",
          "name",
          "description",
          "documentType",
          "documentTypeId",
          "requiresReview",
          "needsAttention",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "null",
              "string"
            ]
          },
          "documentType": {
            "type": "string"
          },
          "documentTypeId": {
            "type": "string"
          },
          "requiresReview": {
            "type": "boolean"
          },
          "needsAttention": {
            "type": "boolean"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ErrorDetailEnvelope": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable code, e.g. missing_scope."
          },
          "message": {
            "type": "string"
          }
        },
        "description": "A refusal with a human-readable explanation beside the stable code."
      },
      "ErrorEnvelope": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable code, e.g. invalid_api_key."
          }
        },
        "description": "The authentication layer's bare envelope; the WWW-Authenticate response header names the same code."
      },
      "InferenceErrorEnvelope": {
        "required": [
          "error"
        ],
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "description": "Stable machine-readable discriminator, e.g. rate_limited."
              },
              "message": {
                "type": "string"
              }
            }
          }
        },
        "description": "The OpenAI-shaped nested envelope inference-path errors use; branch on error.type."
      },
      "JsonElement": { },
      "StartWorkflowRunResponse": {
        "required": [
          "runId",
          "status",
          "startedAt"
        ],
        "type": "object",
        "properties": {
          "runId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateDocumentFieldResponse": {
        "required": [
          "key",
          "label"
        ],
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "TemplateDocumentIssueResponse": {
        "required": [
          "code",
          "message"
        ],
        "type": "object",
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "TemplateDocumentLinks": {
        "required": [
          "self",
          "docx",
          "pdf",
          "markdown"
        ],
        "type": "object",
        "properties": {
          "self": {
            "type": "string"
          },
          "docx": {
            "type": "string"
          },
          "pdf": {
            "type": "string"
          },
          "markdown": {
            "type": "string"
          }
        }
      },
      "TemplateDocumentResponse": {
        "required": [
          "id",
          "templateId",
          "templateName",
          "documentType",
          "title",
          "status",
          "unresolvedFields",
          "issues",
          "reviewed",
          "reviewRequired",
          "formats",
          "links",
          "createdAt",
          "updatedAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "templateId": {
            "type": "string"
          },
          "templateName": {
            "type": "string"
          },
          "documentType": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "unresolvedFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateDocumentFieldResponse"
            }
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateDocumentIssueResponse"
            }
          },
          "reviewed": {
            "type": "boolean"
          },
          "reviewRequired": {
            "type": "boolean"
          },
          "formats": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "links": {
            "$ref": "#/components/schemas/TemplateDocumentLinks"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkflowInputDocumentStatusResponse": {
        "required": [
          "documentId",
          "documentVersionId",
          "filename",
          "contentType",
          "sizeBytes",
          "pageCount",
          "status",
          "processingError",
          "usedByRunId",
          "createdAt"
        ],
        "type": "object",
        "properties": {
          "documentId": {
            "type": "string"
          },
          "documentVersionId": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "sizeBytes": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "pageCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "status": {
            "type": "string"
          },
          "processingError": {
            "type": [
              "null",
              "string"
            ]
          },
          "usedByRunId": {
            "type": [
              "null",
              "string"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkflowInputUploadResponse": {
        "required": [
          "documentId",
          "documentVersionId",
          "filename",
          "sizeBytes",
          "contentType",
          "pageCount",
          "status"
        ],
        "type": "object",
        "properties": {
          "documentId": {
            "type": "string"
          },
          "documentVersionId": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "sizeBytes": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int64"
          },
          "contentType": {
            "type": "string"
          },
          "pageCount": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "format": "int32"
          },
          "status": {
            "type": "string"
          }
        }
      }
    },
    "securitySchemes": {
      "BearerApiKey": {
        "type": "http",
        "description": "Tenant-scoped Ferrith Chat API key. Send as `Authorization: Bearer fck_...`.",
        "scheme": "bearer",
        "bearerFormat": "fck_<base64url>"
      }
    }
  },
  "security": [
    {
      "BearerApiKey": [ ]
    }
  ],
  "tags": [
    {
      "name": "Workflows"
    },
    {
      "name": "Runs"
    },
    {
      "name": "Chat completions"
    },
    {
      "name": "Workflow inputs"
    },
    {
      "name": "Document templates"
    }
  ]
}