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 bydocgen/scripts/export_openapi.pyand 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"}), never403— 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
NotFoundError→404, domainValidationError→422, any other exception →500with the class name prefixed. (Thedocgen.errorsmodule's docstring mapsValidationErrorto400, but the/v1layer maps it to422— trust the422behaviour for this API.) - Some bridge commands report not-found via an
{"error": ...}result rather than raising;/v1normalizes these to404so not-found is uniform. - Cross-org access is reported as
404, never403— 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