QUICKSTART
Start with the skill and a revocable token.
- Download the ttyinv-invoice skill with its SHA-256 checksum, or inspect the Agents page.
- Sign in on the hosted app, create a named token, and store it locally as
TTYINV_AGENT_TOKEN. - Choose MCP or REST. Both create the same canonical
ttyinv/v2Markdown.
https://app.ttyinv.com/api/v1/invoices/render and render_invoice run the same Rust renderer as the CLI and the editor, and answer with HTML, PDF, or PNG bytes.DOCUMENT GRAMMAR
Content first, configuration small.
Frontmatter carries required schema: ttyinv/v2 and these optional configuration keys. JSON and YAML use snake_case names.
| Key | Accepted values | Default |
|---|---|---|
format | code-comma-dot code-dot-comma code-space-comma code-indian code-plain | code-comma-dot |
theme | printable paper-white graphite blueprint ledger-pad solarized-light parchment midnight nord gruvbox-dark | printable |
font | geist-mono cousine fira ibm-plex inconsolata jetbrains roboto source-code space ubuntu | geist-mono |
font-weight | regular semibold | regular |
density | comfortable compact | comfortable |
amount-in-words | true false | false |
accent | lowercase #rrggbb, or absent | theme accent |
font-scale | integer 100–140 | 100 |
frame-inset | integer 30–60 | 54 |
The body is an H1 title, a metadata list, the fixed ## Fromand ## Bill to parties, then ordinary ## sections that each hold exactly one body kind — a table or prose. The optional footer is ## Settlements, ## Payment,## Signature, in that order. Those five names are reserved, so an ordinary section cannot take one.
A directive belongs to the block below it and travels with that block when the section moves: <!-- ttyinv:gap-before none -->and its other levels (tight, standard,roomy), <!-- ttyinv:page-break-before -->, and <!-- ttyinv:summary-only -->. The argument follows the name after a space; there is no colon. Every image requires alt text. The editor renders image paths as bounded placeholders. Only an explicit agent render request can supply validated asset bytes.
Amounts use presets, not locales
Each preset prints the ISO 4217 code before the amount, so a reader of any language reads the same marker. Arithmetic runs on minor units with BigInt; no Intl locale is consulted.
code-comma-dotEUR 1,234.56code-dot-commaEUR 1.234,56code-space-commaEUR 1 234,56code-indianINR 12,34,567.89code-plainEUR 1234.56DISCOVERY
Fetch the live map before you call an endpoint.
Fetch https://app.ttyinv.com/api/v1 first. Then fetch https://app.ttyinv.com/api/v1/invoices/schema for exact input and output schemas.
The discovery response lists service and contract versions, REST paths, MCP tools, authentication, operation availability, status meanings, and response headers.
curl --fail --silent --show-error "https://app.ttyinv.com/api/v1"
AUTHENTICATION
Bearer tokens are private and shown once.
Send Authorization: Bearer $TTYINV_AGENT_TOKEN on MCP and REST requests. Tokens expire, can be revoked immediately, and are stored by ttyinv only as SHA-256 digests.
- Keep tokens in an environment variable or secret manager, not source, command history, URLs, chat, screenshots, or logs.
- Never use verbose HTTP output or shell tracing around authenticated commands.
- A
401means the token is missing, expired, invalid, or revoked. Create a replacement. - A
429means the authenticated request limiter rejected excess requests. A render queue does not use429.
1 · MCP
Prefer MCP when your agent supports tools.
Use the Streamable HTTP endpoint at https://app.ttyinv.com/mcp. Send a Bearer token on every request.
Initialize with MCP initialize over POST. Discover tools with tools/list after initialization.
{
"mcpServers": {
"ttyinv": {
"type": "http",
"url": "https://app.ttyinv.com/mcp",
"headers": { "Authorization": "Bearer $TTYINV_AGENT_TOKEN" }
}
}
}create_invoiceCreate needs no purchase. Its input and output schemas come from the live schema.validate_invoiceValidate needs no purchase. Its input and output schemas come from the live schema.render_invoiceRender needs no purchase. It answers a manifest with the format, page count, dimensions, warnings, and one base64 file.MCP returns JSON errors with the same status meanings as REST. Common statuses include 400, 401, 403, 413, 422, 429, 500, and 503.
2 · REST API
Use strict JSON over ordinary HTTP.
Discovery and authenticated REST operations use Cache-Control: no-store. The public schema uses Cache-Control: public, max-age=300, stale-while-revalidate=3600. Fetch discovery, then fetch the schema.
| Endpoint | Authentication and content type | Input and success output | Purchase | Common failures |
|---|---|---|---|---|
GET https://app.ttyinv.com/api/v1 | None. Server: Returns application/json. | Input: none. Success: 200, discovery document. | None. | None expected. |
GET https://app.ttyinv.com/api/v1/invoices/schema | None. Server: Returns application/json. | Input: none. Success: 200, schema envelope. | None. | None expected. |
POST https://app.ttyinv.com/api/v1/invoices | Bearer token. Client: Send application/json. | Input: kind:"create" with nested draft. Success: 201, {created:{source,document,revision}}. | None. | 400, 401, 403, 413, 415, 422, 429. |
POST https://app.ttyinv.com/api/v1/invoices/validate | Bearer token. Client: Send application/json. | Input: kind:"validate" with tagged source. Success: {validated:{revision,valid,document,diagnostics}}. | None. | 400, 401, 403, 413, 415, 422, 429. |
GET https://app.ttyinv.com/api/v1/invoices/render | Bearer token. Server: Returns application/json. | Input: none. Success: 200, render capabilities. | None. | 401, 403, 429. |
POST https://app.ttyinv.com/api/v1/invoices/render | Bearer token. Client: Send application/json. | Input: kind:"render" with tagged source and nested options. Success: {rendered:{source_revision,plan_digest,bytes,output_sha256,mime,extension,pages,width,height,warnings}}. | None. | 400, 401, 403, 413, 415, 422, 429, 500, 503. |
SCHEMA DISCOVERY
Fetch contracts; do not copy fields into agent prompts.
https://app.ttyinv.com/api/v1/invoices/schema is the authoritative JSON Schema envelope generated from the Rust command registry and shared by REST and MCP. Each operation entry carries its exact input and successful output schemas, and discovery reports the availability of every operation. Structured JSON and YAML accept null only where the Rust type is Option. Fetch the schema before composing requests. Do not copy field lists into prompts.
curl --fail --silent --show-error "https://app.ttyinv.com/api/v1/invoices/schema"
The CLI writes the underlying ttyinv/v2 document schema.
CREATE CONTRACT
Structured input returns source first.
This fabricated payload uses the Rust kind:"create" envelope and nested draft. Consult the live schema for required fields, limits, formats, and newly added capabilities.
{
"kind": "create",
"draft": {
"config": {
"schema": "ttyinv/v2",
"format": "code-comma-dot",
"theme": "printable",
"font": "geist-mono",
"font_weight": "regular",
"density": "comfortable",
"amount_in_words": false,
"accent": null,
"font_scale": 100,
"frame_inset": 54
},
"title": "Fabricated services",
"metadata": {
"number": "INV-EXAMPLE-001",
"kind": "standard",
"issued": "2026-08-24",
"due": "2026-09-07",
"currency": "USD"
},
"from": { "name": "Northstar Example Studio", "email": "billing@example.com" },
"bill_to": { "name": "Acme Example Client" },
"ordinary_sections": [{
"title": "Services",
"body": {
"headings": ["Description", "Amount (USD)"],
"rows": [["Fabricated accessibility review", "125.00"]]
}
}]
}
}curl --fail-with-body --silent --show-error \ --config <(cat <<EOF header = "Authorization: Bearer $TTYINV_AGENT_TOKEN" header = "Content-Type: application/json" EOF ) --data-binary @invoice.json "https://app.ttyinv.com/api/v1/invoices" > created.json
The Rust success outcome is {created:{source,document,revision}}. The adapter builds a document and calls the same canonical serializer as the editor, so source is exactly the canonical form. Save it as the portable artifact.
SOURCE FIDELITY
Keep the authored invoice’s meaning intact.
- Payable currency: when a table has several amount columns, exactly one qualified column matching
metadata.currencyis payable. Conversions are authored explicitly; ttyinv does not fetch exchange rates. - Authored summaries: rows named
Subtotal,Total, orGrand Totalremain visible and are excluded from generated totals. - Recap tables: set
summary_only: truein a section’sdirectivesobject when a table repeats amounts from earlier sections. Its rows remain visible while the section is excluded from generated totals. - Deterministic page breaks: set
page_break_before: truein a section’sdirectivesobject. The returned source emits the exact page-break marker. - Authored spacing: set
gapindirectivestonone,tight,standard, orroomy. The marker stays attached to that section when it moves.
Fetch https://app.ttyinv.com/api/v1/invoices/schema before composing requests so REST and MCP stay aligned.
VALIDATE CONTRACT
Validation is explicit and read-only.
jq -n --rawfile source invoice.md '{
kind:"validate",
source:{markdown:$source}
}' | \
curl --fail-with-body --silent --show-error \
--config <(cat <<EOF
header = "Authorization: Bearer $TTYINV_AGENT_TOKEN"
header = "Content-Type: application/json"
EOF
) --data-binary @- "https://app.ttyinv.com/api/v1/invoices/validate"The Rust success outcome is {validated:{revision,valid,document,diagnostics}}. Continue only when valid is true. When false, document is null and diagnostics identify the errors.
RENDER CONTRACT
One renderer, called over HTTP.
https://app.ttyinv.com/api/v1/invoices/render and the render_invoice tool run the same Rust core renderer that the CLI and the browser editor run, so one document with one set of options gives one file on every surface. Render authorization continues to use the existing invoices:create scope.
GET the same path for the live capability document: output formats, theme identifiers, font identifiers, density values, and every render limit. Read it instead of copying a list into your agent; discovery names the path in rest.operations.render.capabilities.path.
curl --fail-with-body --silent --show-error \ --config <(cat <<EOF header = "Authorization: Bearer $TTYINV_AGENT_TOKEN" header = "Content-Type: application/json" EOF ) --request GET "https://app.ttyinv.com/api/v1/invoices/render"
POST the Rust kind:"render" envelope. Put the tagged source and nested options object in the request. The options use font_weight, font_scale, frame_inset, and other snake_case keys.
jq -n --rawfile source invoice.md '{
kind:"render",
source:{markdown:$source},
options:{
format:"pdf",
theme:"printable",
font:"geist-mono",
font_weight:"regular",
density:"comfortable",
accent:null,
font_scale:100,
frame_inset:54,
png_scale:1
}
}' | \
curl --fail-with-body --silent --show-error \
--config <(cat <<EOF
header = "Authorization: Bearer $TTYINV_AGENT_TOKEN"
header = "Content-Type: application/json"
EOF
) --data-binary @- "https://app.ttyinv.com/api/v1/invoices/render" | \
jq -r '.rendered.bytes' | base64 --decode > invoice.pdfThe Rust success outcome is {rendered:{source_revision,plan_digest,bytes,output_sha256,mime,extension,pages,width,height,warnings}}. Native and WASM outcomes keep byte buffers raw. REST encodes bytes as base64 and output_sha256 as lowercase hexadecimal. The Rust and WASM output limit is 64 MiB.
The editor renders image paths as bounded placeholders. An explicit agent render request may include validated asset bytes under options.assets; the renderer never fetches an image. An oversize source or output answers 413. A full render queue or worker failure answers 503. The authenticated request limiter answers 429.
3 · CLI · LOCAL-FIRST
Use the CLI when contents must not cross the network.
The CLI is a Rust binary over the same core the browser runs as WebAssembly, so a document that validates in one validates identically in the other, and a document rendered by one matches the other. Build it from the public repository with make rust-release. There is no runtime to install.
ttyinv validate invoice.md --json ttyinv sections invoice.md --json ttyinv render invoice.md --format pdf --output invoice.pdf ttyinv edit move-section invoice.md --from 3 --to 1 --stdout ttyinv edit set-gap invoice.md --section 2 --gap roomy --check
Every CLI command
ttyinv validate FILE [--json]validates the document and reports diagnostics with their codes.ttyinv schema [--output FILE]writes thettyinv/v2document schema.ttyinv sections FILE [--json]lists the ordinary sections with position, title, body kind, and gap token.ttyinv render FILE --format html|pdf|png [--output FILE|--stdout]writes the finished invoice with the shared renderer, and accepts--theme,--font,--font-weight,--density,--accent,--font-scale,--frame-inset, and--asset-base.ttyinv edit move-section FILE --from N --to Nmoves one ordinary section, carrying its directives with it.ttyinv edit set-gap FILE --section N --gap none|tight|standard|roomysets the gap above one ordinary section.
Both edit commands accept --stdout to print instead of writing, --check to exit non-zero when the document would change, and --json for the full engine response. CLI section positions are one based. In-place edits write a sibling temporary file and rename. Status 5 means the render itself failed after the input and the options were accepted.
PRIVACY & DATA FLOW
Choose the boundary deliberately.
Use fabricated example.com identities for testing. Never commit a real invoice, customer record, signature, or credential.