Skip to content

DocGen /v1 Platform API — integrator reference

The canonical, code-grounded reference for the headless /v1 REST surface. /v1 is a thin, versioned layer over the DocGen engine: instantiate templates, configure and read documents, generate, stream progress, and export .docx.

  • Machine-readable contract: openapi-v1.json (OpenAPI 3.1, regenerated by docgen/scripts/export_openapi.py and drift-tested). Paths, request bodies, status codes, and the bearer auth scheme are fully typed. Response bodies are concretely typed for an initial set of endpoints (GET /whoami, POST …/generate); the rest currently export an open object — full response-schema coverage is tracked as a follow-up (#362).
  • Task-oriented guide (authoring, spec shape, worked flows): ../how-to/authoring-and-config-api.md.
  • Stability & deprecation policy: versioning.md.

This doc describes what the code does today (docgen/src/docgen/api_v1.py, server.py, auth_dev.py, progress.py). Where behaviour is quirky, it is called out rather than smoothed over.


Base URL

/v1 is mounted on the DocGen FastAPI service (docgen.server:app). All routes below are relative to that host, prefixed with /v1.

Environment Base URL
Local dev http://127.0.0.1:8769 (uv run uvicorn docgen.server:app --host 127.0.0.1 --port 8769)
Deployed the DocGen service host (behind your gateway)

Interactive OpenAPI/Swagger UI is served at /docs on the same host. Note that /docs renders the full running service (docgen.server:app mounts /v1 alongside the internal /bridge, /health, and dev-login routes, under the app's own default metadata) — it is not the /v1-only contract. The pinned, integrator-facing contract is the committed openapi-v1.json.


Authentication

Current state (honest): /v1 sits behind altaforge.auth.AuthMiddleware with a JwtAuthBackend. Every /v1 request needs a bearer JWT:

Authorization: Bearer <token>
  • Deployed: an interactive Azure-AD (Entra ID) user token. The backend validates signature, audience, issuer, and expiry against the tenant JWKS (configured via AZURE_APP_CLIENT_ID / AZURE_ISSUER_URL / AZURE_JWKS_URL).
  • Local dev: a faked issuer, but a real RS256 token flow. POST /auth/dev-login (no body, no password) mints a signed token:
// POST /auth/dev-login  ->  200
{ "token": "<jwt>", "tokenType": "Bearer" }

The same JwtAuthBackend validates it end-to-end against a locally served JWKS (GET /auth/jwks). Only the issuer and the password-less login are faked; the validation path is identical to production. (docgen/src/docgen/auth_dev.py.)

Machine-to-machine (M2M) auth is NOT available yet. There is no client- credentials / service-principal grant on /v1 today — every call is on behalf of an interactive user. M2M is a tracked follow-up: issue #352. Do not build against an M2M flow that does not exist.

Unauthenticated paths (no token required): /health, /auth/jwks, /auth/dev-login. Everything under /v1 requires a valid token.

Selecting an organization

DocGen is multi-tenant. A caller may belong to several orgs; pick the active one per request with a header:

X-Org-Slug: <org-slug>
  • Omitted → the backend's default org resolution applies.
  • A slug not in the caller's permitted orgs → 404 ({"detail": "organization not found"}), never 403 — this is deliberate, to avoid org enumeration.

Confirm what DocGen resolved from your token with GET /v1/whoami:

// GET /v1/whoami  ->  200
{
  "user": { "email": "you@example.com", "organizations": ["acme", "globex"] },
  "activeOrg": "acme"
}

Identity does not stop at DocGen: for data sources served over per-user MCP, the verified caller (bearer + email) is forwarded to the source server, so gathered data is scoped to the authenticated user.


Endpoints

All paths are under /v1. Bodies are JSON unless noted. Field values are stored as strings by the engine (the Field model is string-typed), so numbers and dates round-trip as their string form.

Identity & catalog (read)

Method Path Purpose
GET /whoami The verified caller + active org (proof auth reached DocGen).
GET /templates Templates the caller can instantiate.
GET /texts Org-configured Texts (phrase atoms).
GET /snippets Org-configured Snippets (clause molecules).
GET /risk-blocks Org-configured Risk Blocks (statutory analysis).
GET /orchestration-rules The orchestration rulebook.
GET /config-schema Self-describing config surface — the authoring spec's JSON schema + config-group keys.

Documents

Method Path Purpose
GET /documents List the caller-org documents, each enriched with its {key: value} field map.
GET /documents/{document_id} One document: header + sections (+ sources). 404 if absent.
POST /documents Instantiate a template into a new document. 201. Body: CreateDocumentBody.
PUT /documents/{document_id}/fields Set/patch field values; returns the refreshed document. Body: { "fields": { ... } }.
PUT /documents/{document_id}/state Set lifecycle state. Body: { "state": "draft" \| "under-review" \| "finalized" }.
GET /documents/{document_id}/readiness Readiness verdict + gaps (can this document generate?).

CreateDocumentBody:

{
  "templateId": "tmpl_...",     // required
  "title": "Q3 Firm-Order Memo", // optional
  "fields": { "deal_id": "614563" } // optional; values coerced to strings
}

Config & spec (authoring)

Method Path Purpose
GET /documents/{document_id}/config Read the full document/template config object.
PUT /documents/{document_id}/config Atomic partial config patch; returns resolved config.
GET /documents/{document_id}/spec Export a template/document to canonical spec JSON.
PUT /documents/{document_id}/spec Apply a full spec to update a template in place (spec = desired state).
POST /templates Create a template by compiling a provided spec. 201. ?dry_run=true validates + reports without writing.

See ../how-to/authoring-and-config-api.md for the spec shape and the read-modify-write loop.

Generate, gather & progress

Method Path Purpose
POST /documents/{document_id}/orchestrate Run the registered orchestrator over source_text (extract → decide → set fields).
POST /documents/{document_id}/gather Run the source lane only (no synth/render LLM step); returns gathered payloads.
POST /documents/{document_id}/generate Start async generation; returns immediately. Body: GenerateBody.
GET /documents/{document_id}/progress SSE feed of generate-pipeline progress (see below).
GET /documents/{document_id}/export.docx Download the composed .docx (binary; Content-Disposition: attachment).

GenerateBody:

{
  "variables": {},          // render variables (optional)
  "force": false,           // overwrite hand-edited sections
  "preserveEdited": false   // regenerate the rest, keep edited sections
}

POST …/generate returns:

// 200
{
  "started": true,
  "alreadyRunning": false,
  "documentId": "doc_...",
  "progressUrl": "/v1/documents/doc_.../progress"
}

Generation runs on a worker thread; completion is signaled on the SSE feed, not on this response. Subscribe to progressUrl after receiving started.

Confirm-before-overwrite (409). If the document has hand-edited sections and you pass neither force nor preserveEdited, generation is refused with:

// 409
{ "detail": { "blockedEdited": true, "editedSectionTitles": ["Executive Summary"] } }

Re-issue with force: true (overwrite all) or preserveEdited: true (keep edited, regenerate the rest).

Sections

Method Path Purpose
PUT /documents/{document_id}/sections/{section_id} Edit one section (only sent keys change). 422 if body is empty.
POST /documents/{document_id}/sections/{section_id}/regenerate Regenerate one section.

PUT …/sections/{id} accepts any subset of: title, content, status, render_mode, prompt, output_template, output_type.

Version history

Method Path Purpose
GET /documents/{document_id}/snapshots List document snapshots (whole-document output versions), newest first.
POST /documents/{document_id}/snapshots Capture current output as a snapshot. 201. Body: { "label"?: "..." }.
POST /documents/{document_id}/snapshots/{snapshot_id}/restore Restore a snapshot onto the live sections.
GET /documents/{document_id}/sections/{section_id}/versions A section's output history, newest first.
POST /documents/{document_id}/sections/{section_id}/versions/{version_num}/restore Restore a past section output.
GET /documents/{document_id}/generation-snapshots Immutable per-render pins, newest first.
GET /documents/{document_id}/generation-snapshots/{snapshot_id} One generation snapshot: full manifest + output.
POST /documents/{document_id}/generation-snapshots/{snapshot_id}/reproduce Re-render from the pinned manifest (templated → byte-identical).

Snapshots restore onto live rows; generation snapshots are read-only pins you reproduce from, never restore.


Progress SSE event model

GET /v1/documents/{document_id}/progress is a text/event-stream. Each frame is:

data: {"event":"synthesize_started","seq":5,"runId":1,"ts":1721500000.12}\n\n

Every event carries seq (monotonic per document; poll/reconnect with the last seq you saw), runId (bumped on each new run — a change means a fresh run), and ts (epoch seconds). The pipeline is gather → compute → synthesize → render → verify; the lifecycle events, in order:

event When Extra fields
pipeline_started Run begins
pipeline_blocked Pre-generation readiness gate refused (missing required fields / insufficient data). Terminal — the stream closes. readiness (verdict + gaps)
gather_started Source lane begins
need_started / need_finished / need_failed / need_cached Per data-need during gather dataNeedId, outputKey, kind; elapsedMs on finished/failed
orchestrate_started Orchestrator dispatch
synthesize_started Batched synthesis mega-call
render_started Per-section render
verify_started Citation fact-check phase
verify_section_started / verify_section_finished Per cited section during verify sectionId
pipeline_finished Terminal — success. readiness (placeholder completeness), unsupported_sections
pipeline_failed Terminal — error. error (message string)

Terminal events are pipeline_finished, pipeline_failed, and pipeline_blocked (docgen.progress.TERMINAL_EVENTS). The server closes the stream after emitting any one of them — a blocked run ends the stream exactly like a success or a failure. So a run that hits the pre-generation readiness gate emits pipeline_blocked as its final frame and the connection closes; you never wait for a terminal frame that will not come. On pipeline_blocked, read readiness, surface the gaps to the user, and stop — the stream is already ending on its own.

The stream also ends after 1 hour of silence (idle timeout). Poll cadence is ~0.25s.


Error shapes

/v1 uses FastAPI's standard error envelope. The body is always {"detail": ...}; detail is a string for most errors, and a structured object or array for the two cases below.

Status Meaning detail
401 Missing / invalid / expired bearer token string (from AuthMiddleware)
404 Entity not found in the caller's org scope, or an X-Org-Slug outside the caller's orgs string, e.g. "document 'doc_x' not found" / "organization not found"
409 Generate blocked by hand-edited sections { "blockedEdited": true, "editedSectionTitles": [...] }
422 Domain validation error, or request-body validation string (domain) or the standard FastAPI validation array [{ "loc": [...], "msg": "...", "type": "..." }]
500 Unhandled error string "<ExceptionType>: <message>"

Notes grounded in api_v1.py:

  • The domain NotFoundError404, domain ValidationError422, any other exception → 500 with the class name prefixed. (The docgen.errors module's docstring maps ValidationError to 400, but the /v1 layer maps it to 422 — trust the 422 behaviour for this API.)
  • Some bridge commands report not-found via an {"error": ...} result rather than raising; /v1 normalizes these to 404 so not-found is uniform.
  • Cross-org access is reported as 404, never 403 — you cannot distinguish "absent" from "not yours".

A minimal integration flow

1. POST /auth/dev-login                         -> { token }           (local only)
2. GET  /v1/whoami            (Bearer, X-Org-Slug)  -> confirm identity + org
3. GET  /v1/templates                            -> pick a templateId
4. POST /v1/documents         { templateId, fields } -> { document_id }  (201)
5. GET  /v1/documents/{id}/readiness             -> fix any gaps
6. POST /v1/documents/{id}/generate { }          -> { started, progressUrl }
7. GET  /v1/documents/{id}/progress  (SSE)       -> until pipeline_finished / _failed / _blocked
8. GET  /v1/documents/{id}/export.docx           -> download the .docx