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 stable code and the path of the offending value. A 422 means “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 · UNAPPROVED watermark 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 limits key is rejected as unknown. The request body is capped at 4 MiB.
  • Rendering does not write values back to the document — supplying inputs changes 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.