> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Typewriter documents

> The nine routes that compose, store, approve and hand off a document: live preview, the hashed PDF, the approval chain, and the idempotent submit to intake.

# 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.

<Note>
  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.
</Note>

## Compose

### `POST /typewriter/preview`

```json theme={null}
{ "doc_type": "supplier_vat_invoice", "payload": { "supplier_name": "…" } }
```

```json theme={null}
{ "html": "<article>…</article>" }
```

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/documents` → `201`

```json theme={null}
{ "doc_type": "service_invoice", "payload": { }, "supersedes_id": null }
```

Renders the document, hashes the **exact bytes**, stores the PDF, and inserts the row.
Nothing else in the body is read.

```json theme={null}
{ "document": { "id": "…", "doc_type": "service_invoice", "version": 1, "status": "PENDING", "pdf_hash": "…" } }
```

`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.

| Query      | Default | Notes                    |
| ---------- | ------- | ------------------------ |
| `limit`    | `100`   | **clamped, not trusted** |
| `offset`   | `0`     |                          |
| `status`   | -       |                          |
| `doc_type` | -       |                          |

<Note>
  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.
</Note>

### `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.

| Response header | Value                 |
| --------------- | --------------------- |
| `ETag`          | the stored `pdf_hash` |
| `Cache-Control` | `private, no-store`   |

<Warning>
  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.
</Warning>

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`

```json theme={null}
{
  "chain": [ { "step": 1, "role": "*", "approver_id": "…", "approved_at": "…" } ],
  "status": "approved",
  "viewer_may_approve": false,
  "blocked_reason": "you approved step 1"
}
```

`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.

<Note>
  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.
</Note>

### `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.

```json theme={null}
{
  "handoff_id": "…",
  "intake_id": "…",
  "outcome": "created",
  "created_at": "…",
  "duplicate_of": null,
  "duplicate_of_version": null
}
```

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](/greatbook/api/intake), then the posting gate.
See [Typewriter to posted journal entry](/greatbook/technical/typewriter-to-ledger) for the full technical chain and the deployment evidence boundary.

## The error contract

One rule, applied by a single handler:

| Case                        | Response                                                        |
| --------------------------- | --------------------------------------------------------------- |
| an error this module raised | its own status, and a message written for the person reading it |
| **anything else**           | `500`, with no detail                                           |

That asymmetry is deliberate.
A database driver error names columns and can quote values, and these responses reach a browser.

## Where it is enforced

| Concern                                                     | Code                                             |
| ----------------------------------------------------------- | ------------------------------------------------ |
| the HTTP mapping and the error rule                         | `langgraph_chat/devcenter/typewriter_api.py`     |
| every effect - render, hash, store, insert, approve, submit | `langgraph_chat/devcenter/typewriter_service.py` |
| the renderer and its document templates                     | `renderer/`                                      |
| the approval chain's completeness, asserted at boot         | `typewriter_api`'s boot assertion                |

## 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.

## Related

* [Compose a document](/greatbook/guides/compose-a-document) - the same flow in the UI
* [Approve a document](/greatbook/guides/approve-a-document) - the approval chain from a signer's seat
* [The Typewriter](/greatbook/agents/typewriter) - the agent lane, and why it is propose-only
* [Composition and rendering](/greatbook/capabilities/composition-and-rendering) - the hashed-PDF guarantee
* [Typewriter to posted journal entry](/greatbook/technical/typewriter-to-ledger) - the exact stores, identities, refusals and human gates
