Skip to main content

Typewriter documents

The Typewriter source surface defines 146 schema-driven document types, an interactive preview, a stored PDF whose exact bytes are hashed, an approval chain, and a hand-off into the Clerk’s intake queue. Its routes and controls are verified in source, but the complete workflow has not been exercised end to end for this documentation cut. All routes are prefixed /typewriter, require a verified identity, and are org-scoped. Provenance columns are set server-side from the verified session - the body never carries who created or approved anything.
Two identities are derived from the same session, because these tables key a person two ways: the actor string for the audit trail, and the session uuid the row-level-security policies compare against. A service-token caller has no browser session and therefore no uuid, so a route that must write one returns 400 rather than inventing it.

Compose

POST /typewriter/preview

Persists nothing, and validates nothing beyond structure. A partial or empty payload is a valid preview that renders blanks, never an error - the whole point is to show the document taking shape as it is typed. The route still requires an attributable caller, through the same dependency every other route here uses, rather than being the one endpoint in the module any authenticated request can drive.

POST /typewriter/documents201

Renders the document, hashes the exact bytes, stores the PDF, and inserts the row. Nothing else in the body is read.
supersedes_id is how a correction works: a new version that names the one it replaces. The superseded document is not edited. For every currency-bearing document, preview/save validates the payload against greatbook.currency-code/v1 before render, storage or persistence. The canonical enum contains 307 exact uppercase ISO 4217 current and historical assignments. An unsupported value returns a typed 422 with code unsupported_currency and field currency; lowercase codes, localized aliases, symbols and invented codes are not normalized into a different accounting assertion.

Read

GET /typewriter/documents

The documents table - one entry per document, with every version of it.
An unbounded page size on a table carrying each document’s whole version history is a request a browser can make that costs the database far more than it costs the browser. So the limit is clamped server-side rather than validated and rejected.

GET /typewriter/documents/{id}

One version: its payload, its approval trail, and its hand-offs.

GET /typewriter/documents/{id}/json

The self-describing JSON envelope, served as an attachment with a filename - this is a file someone keeps, and a bare JSON body rendered in a browser tab is not the artifact they asked for.

GET /typewriter/documents/{id}/pdf

Streams the stored PDF after authorisation.
This route is the access control. The storage bucket is private and carries no permissive object policies, so the org-scoped lookup is what decides, and the bytes are fetched with the service role only once that lookup has succeeded. There is no public storage URL for a GreatBook document.
The ETag is the hash of exactly these bytes, and it is what the approval trail attests to - so a recipient can verify the file they downloaded is the file that was approved, without asking anyone.

Approve

GET /typewriter/documents/{id}/approval

viewer_may_approve and blocked_reason exist so the UI can render a control that would be refused as a rule rather than as a button that fails.
The trail is attributable rather than anonymous. Every completed step carries the recorded approver user id and approval time. For the final step, including the common single-step chain, the document read also resolves approved_by_identity through the trusted identity store when available. The UI shows that resolved identity or labels the recorded stable user id; it never invents a person’s name. Approval-sensitive Books registers keep the same exact checker identity, time and step evidence and report missing evidence as unavailable.

POST /typewriter/documents/{id}/approve

Takes no body. There is nothing for the caller to choose: which step is next is a fact about the chain and the trail, and who is approving is a fact about the session. A body would only offer both as things to get wrong. The stored bytes are re-hashed before the step is recorded, so an approval attests to the file as it is now, not as it was when the page loaded.

Hand off

POST /typewriter/documents/{id}/submit

Projects an approved document directly into the Clerk’s intake queue.
Idempotent through the intake queue’s own partial unique index, so a double-submit resolves to the same intake row and is reported as outcome: "duplicate" - rather than as either a second row or a silent no-op. This service does not re-run the Clerk graph: it writes the schema payload as intake_queue.payload with status pending, unless a live version-group conflict makes it flagged. There is no second classification, extraction or Teable master-resolution pass at handoff. From there the document takes the normal path: the intake gate, then the posting gate. See Typewriter to posted journal entry for the full technical chain and the deployment evidence boundary.

The error contract

One rule, applied by a single handler: That asymmetry is deliberate. A database driver error names columns and can quote values, and these responses reach a browser.

Where it is enforced

The test that would fail if it broke

langgraph_chat/devcenter/tests/test_typewriter_api.py covers the nine routes, including the clamped limit, the authorise-then-fetch ordering on the PDF route, and the idempotent double-submit.