The Document API
Authentication, the generation endpoint, the input format, error semantics, and what gets billed.
The Document API produces documents from outside the app: your system supplies values, Final Wording applies them to the governed template. It is deliberately small — render a PDF, resolve or create a document from a template, and the schema that describes the inputs.
Authentication
Authorization: Bearer <workspace_api_token>
A token is scoped to one workspace. Treat it as workspace-wide read-and-render power: a holder can discover and render any document version in that workspace and can supply arbitrary input values. Store it accordingly — it belongs in a secrets manager, not in a repository.
A workspace admin issues tokens under Workspace settings → API tokens: create a named token — name it after the integration that will hold it — and copy the secret, which is shown exactly once and stored only as a hash. The same list shows each token’s creator, its last use, and what it has generated, and revokes a token with immediate effect. Rotation is deliberate overlap: create the replacement, move your callers, then revoke the old token — two active tokens are expected during the swap.
A missing, invalid or revoked token is a 401; a version outside the token’s
workspace is a 404, indistinguishable from a version that does not exist.
Generate a PDF
POST /api/v1/document-generations/pdf
Authorization: Bearer <workspace_api_token>
Idempotency-Key: <optional>
Content-Type: application/json
{
"document_version_key": "<document_version_key>",
"inputs": { "fields": { "customer_name": { "textValue": "Grace Hopper" } } }
}
A successful render returns the PDF itself:
200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="<document name>.pdf"
inputs is optional. When omitted, the version renders from the values saved
on it. When present, it replaces the saved values for this render only:
nothing is written back to the document, and there is no partial merge — a
present inputs object is the complete value set, so a required field that is
saved on the version must still be supplied again.
The input format
inputs is an object with a single fields map, keyed by the field names the
template publishes. Every value states its type explicitly:
| Template field type | Value |
|---|---|
| text | {"textValue": "…"} |
| yes/no | {"booleanValue": true} |
| decimal | {"decimalValue": "1200.50"} — exact, as a string |
| date | {"dateValue": "2026-07-28"} — ISO date, no zone |
| object | {"objectValue": {"fields": { … }}} |
| list | {"listValue": {"items": [ … ]}} |
| null (optional) | {"nullValue": true} |
The authoritative contract is machine-readable — generate your types from it instead of hard-coding field names:
GET /api/v1/document-versions/{document_version_key}/input-schema
Authorization: Bearer <workspace_api_token>
returns a JSON Schema (draft 2020-12) describing exactly the fields this version accepts, including which are required and how each is typed.
Create a document from a template
Two endpoints work with a pinned template version and natural bindings —
plain JSON values keyed by field name, without the typed wrappers:
POST /api/v1/document-versions/{document_version_key}/resolve
POST /api/v1/document-versions/{document_version_key}/documents
resolve applies your bindings and reports every diagnostic without creating
anything — the dry run an integration tests against. documents creates one
ordinary editable document (plus its initial version) in an explicitly named
project; project_key and a nonblank name are required, and so is an
Idempotency-Key, because creating twice is worse than rendering twice. The
created document has its conditional and repeat logic fully materialized: it
is a normal document from then on, and goes through comments, approvals and
generation like one.
Creation carries one extra boundary the render endpoints do not: the token’s
linked user must currently hold Editor-or-higher access to the destination
project, and creation fails closed with a 403 if that access is gone.
Errors mean two different things
400,401,404,405— the request itself is wrong: malformed JSON, unknown fields, a bad token, a wrong URL. Fix the caller.422 Unprocessable Entity— the request was fine, but the document cannot be resolved with these values: a required field missing, a malformed date, a value of the wrong kind. The body lists every discoverable failure as a diagnostic with a stablecodeand the path of the offending value. A422means “fix the inputs or the document”, never “retry” — the same request will fail the same way.500— an unexpected render failure on our side. Retrying is reasonable.
One nuance worth knowing: an omitted optional list is a valid empty
collection — the repeat simply produces zero occurrences. An explicit null
is a different statement and is rejected as a repeat source.
Billing and retries
A render is billed only after it succeeds: one charge per generated document
plus one per rendered page, counted from page one. A failed render — any
4xx or 5xx — is never billed.
Send an Idempotency-Key on requests you might retry. A retried key
re-renders and returns the PDF again but records no second charge, so a
timeout-and-retry loop cannot double-bill.
What the API deliberately does not do
- It does not wait for approval. A version whose required approvals are
incomplete still renders — with the same
DRAFT · UNAPPROVEDwatermark the in-app export would carry. - It does not accept resolution limits from the caller: repeat depth, item
counts and output size are bounded by the server alone, and a
limitskey is rejected as unknown. The request body is capped at 4 MiB. - Rendering does not write values back to the document — supplying
inputschanges one render, not the record. Creating a document is its own explicit endpoint, never a side effect of rendering.
A request that works end to end
curl -sS -X POST https://app.finalwording.com/api/v1/document-generations/pdf \
-H "Authorization: Bearer $FW_API_TOKEN" \
-H "Idempotency-Key: invoice-2026-08-04-0001" \
-H "Content-Type: application/json" \
-d '{
"document_version_key": "'"$VERSION_KEY"'",
"inputs": { "fields": {
"customer_name": { "textValue": "Grace Hopper" },
"signed_on": { "dateValue": "2026-07-28" },
"fee": { "decimalValue": "1200.50" }
} }
}' \
-o contract.pdf
$VERSION_KEY names the approved document version your template lives on;
the response in contract.pdf is the same PDF the in-app export of that
version would produce with those values.