Beta

API reference

Every route, field, limit and error of the Koutei PDF API. New here? Start with the quickstart.

Overview

  • Base URL: https://kouteisystems.com. Every path starts with /api/v1/.
  • Requests and answers are JSON in UTF-8, except the PDF itself. Times are in UTC, such as 2026-10-12T09:14:03Z.
  • The API version is 2026-10-01. Send it as Koutei-Version: 2026-10-01; every answer repeats it. See Versions.
  • The same description is available to machines as OpenAPI 3.1 at https://kouteisystems.com/api/v1/openapi.json.
  • Only the public routes can be called from a web page. The routes that take a key send no CORS headers, because a key belongs on a server.

Keys and scopes

Send your key in the Authorization header: Authorization: Bearer kt_live_…. A key is never accepted in the URL or the body. You create keys in Settings, up to 3 active at a time. A key is shown once and stored only as a hash, and your email address must be confirmed before one is issued.

A key has scopes. Each scope allows:

Scopes
ScopeAllows
documents:createPOST /api/v1/documents with mode "final"
documents:previewPOST /api/v1/documents with mode "preview"
documents:readGET /api/v1/documents
GET /api/v1/documents/{id}

GET /api/v1/documents/{id}/file
account:readGET /api/v1/account

POST /api/v1/documents/validate works with any valid key, whatever its scopes. Settings offers two kinds of key:

Kinds of key
KindScopesCan use credits
Full accessdocuments:create, documents:preview, documents:read, account:readYes
Preview onlydocuments:preview, documents:read, account:readNo

A revoked or regenerated key stops working from the next request at the latest. A request that is still running when its key is revoked may be refused before it is charged.

Routes

Routes
RouteKeyWhat it does
GET /api/v1/templatesnone (public)List the templates the API offers.
GET /api/v1/templates/{slug}none (public)One template, with the JSON Schema of its fields, its rules and an example request.
GET /api/v1/openapi.jsonnone (public)This API, described as OpenAPI 3.1 JSON.
POST /api/v1/documentsdocuments:create or documents:previewRender one document and return it.
POST /api/v1/documents/validateany valid keyRun exactly the checks POST /documents runs before rendering. No render, no credit.
GET /api/v1/documentsdocuments:readThe records of the documents your account made through the API in the last 90 days, newest first.
GET /api/v1/documents/{id}documents:readOne document's stored metadata (never the file or its fields).
GET /api/v1/documents/{id}/filedocuments:readReserved. Always 404 file_not_available, because Koutei stores no documents.
GET /api/v1/accountaccount:readPlan, credits, the calling key and its limits.

Every route takes the Koutei-Version header, except GET /api/v1/openapi.json. A method a path does not take answers 405 method_not_allowed. Any other path under /api/v1/ answers 404 not_found.

GET /api/v1/templates

List the templates the API offers. Public. Sends Access-Control-Allow-Origin: *, Vary: Koutei-Version, and may be cached for 5 minutes. An OPTIONS preflight gets 204 with Access-Control-Allow-Methods: GET, OPTIONS and Access-Control-Allow-Headers: Koutei-Version.

Key: none (public). Success: 200. It can also answer 400, 404, 429, 500.

GET /api/v1/templates/{slug}

One template, with the JSON Schema of its fields, its rules and an example request. Public. Sends Access-Control-Allow-Origin: *, Vary: Koutei-Version, and may be cached for 5 minutes. An OPTIONS preflight gets 204 with Access-Control-Allow-Methods: GET, OPTIONS and Access-Control-Allow-Headers: Koutei-Version.

Key: none (public). Success: 200. It can also answer 400, 404, 429, 500.

Parameters of GET /api/v1/templates/{slug}
ParameterWhereWhat it is
slugpath, requiredA template name from the list of templates.

GET /api/v1/openapi.json

This API, described as OpenAPI 3.1 JSON. Public. Sends Access-Control-Allow-Origin: *, Vary: Koutei-Version, and may be cached for 5 minutes. An OPTIONS preflight gets 204 with Access-Control-Allow-Methods: GET, OPTIONS and Access-Control-Allow-Headers: Koutei-Version.

Key: none (public). Success: 200. It can also answer 404, 429, 500.

POST /api/v1/documents

Render one document and return it. mode final needs documents:create and costs 1 credit, charged only after the PDF exists; mode preview needs documents:preview and is free and watermarked. The response is the PDF unless the Accept header lists application/json and lists none of application/pdf, application/* or */*; errors are always JSON. Every response states credit_used.

Key: documents:create or documents:preview. Success: 201. It can also answer 400, 401, 402, 403, 404, 409, 413, 415, 422, 429, 500, 502, 503, 504.

Parameters of POST /api/v1/documents
ParameterWhereWhat it is
Idempotency-Keyheader1 to 255 visible ASCII characters; a double-quoted value (IETF draft form) is read without the quotes. Per account, 24 hours. Same key and same normalised request (which includes reference) replays the document free; same key and a different request gets 422 idempotency_key_reused. A taken-over attempt can no longer commit, so at most one document is ever charged per key.

POST /api/v1/documents/validate

Run exactly the checks POST /documents runs before rendering. No render, no credit. Any valid key. 200 when the request would render; 422 validation_failed, with every problem listed, when it would not. Idempotency-Key is ignored.

Key: any valid key. Success: 200. It can also answer 400, 401, 404, 409, 413, 415, 422, 429, 500, 503.

GET /api/v1/documents

The records of the documents your account made through the API in the last 90 days, newest first.

Key: documents:read. Success: 200. It can also answer 400, 401, 403, 404, 429, 500, 503.

Parameters of GET /api/v1/documents
ParameterWhereWhat it is
referencequeryExact, case-sensitive match on the reference label. 1 to 128 characters
limitqueryHow many documents to return. 1 to 100; default 20
starting_afterqueryA document id from the previous page; it must be one of your listable documents. at most 64 characters

GET /api/v1/documents/{id}

One document's stored metadata (never the file or its fields).

Key: documents:read. Success: 200. It can also answer 400, 401, 403, 404, 429, 500, 503.

Parameters of GET /api/v1/documents/{id}
ParameterWhereWhat it is
idpath, requiredA document id, as returned in Koutei-Document-Id.

GET /api/v1/documents/{id}/file

Reserved. Always 404 file_not_available, because Koutei stores no documents. The file is returned only in the response to POST /api/v1/documents. Within 24 hours, repeat that request with the same Idempotency-Key to get it again for free. A later API version may serve files here; that would be additive.

Key: documents:read. It has no success answer. It can also answer 400, 401, 403, 404, 429, 500, 503.

Parameters of GET /api/v1/documents/{id}/file
ParameterWhereWhat it is
idpath, requiredA document id, as returned in Koutei-Document-Id.

GET /api/v1/account

Plan, credits, the calling key and its limits.

Key: account:read. Success: 200. It can also answer 400, 401, 403, 404, 429, 500, 503.

The request

The body of POST /api/v1/documents and POST /api/v1/documents/validate is one JSON object, sent with Content-Type: application/json:

Request body
NameRequiredAcceptsWhat it is
templateYesone of "invoice", "quote", "receipt"Which document to make.
modeYesone of "final", "preview"There is no default: a request never spends a credit unless it says "final". A preview is free and watermarked.
fieldsYesan objectThe details of the document. Each template has its own fields, listed below.
template_versionNoa major version as digits, such as "2"Major version pin, digits only.
referenceNo1 to 128 charactersThe caller's own label for this document, never printed. Stored in the 90-day history, echoed as Koutei-Reference, filterable with GET /documents?reference=. Printable ASCII, no leading or trailing space.
paperNoone of "a4", "letter"; default "a4"The page size.
localeNoone of "en"; default "en"BCP 47. Only en in 2026-10-01; other values get 400 invalid_request.
outputNoonly {"format":"pdf"}Only {"format": "pdf"} (exact, case-sensitive), the same as leaving it out. The planned formats "pdf-a3" and "xml", and every other member of output, are reserved (400 reserved_field); any other value, such as "PDF", is 400 invalid_request (unsupported_value).

A name the API does not know is refused with 400 invalid_request; nothing is silently dropped. A null for an optional parameter is 400 invalid_request (wrong_type; for template_version, invalid_template_version); it is not the same as leaving the parameter out.

Reserved names

These names are held for later versions. Today each is refused with 400 reserved_field, so a name is never ignored now and given a meaning later.

At the top level of the body: callback_url, webhook, file_url, einvoice, validation, jurisdiction, metadata, logo.

Inside output, only {"format":"pdf"} is accepted today. The format values "pdf-a3" and "xml" are reserved, and so is every other member of output, such as pdf_standard, standard and attachments.

Inside fields, for every template: seller, buyer, payment, document, items[].tax.

Templates

You send facts in fields; the template lays them out. GET /api/v1/templates/{slug} returns the same field list as a JSON Schema, with an example request.

invoice · Invoice, version 2.0.0

An invoice a seller issues to a client: 1 to 50 line items, an optional discount, one optional tax line, an optional amount already paid and the balance due. Paper: "a4" or "letter".

Fields of the invoice template
FieldTypeRequiredNotes
seller_nametextYes
seller_detailsmultilineNo
tax_idtextNoPrinted as "Tax / VAT No." under the seller.
client_nametextYesPrinted under "Bill to".
client_detailsmultilineNo
invoice_numbertextYesYou supply it; Koutei never allocates or checks numbers.
datedateYesIssue date.
due_datedateNo
po_numbertextNoThe buyer's purchase order number only, printed "PO no.".
related_documenttextNoNumber of the document this invoice follows (e.g. the quote), printed "Ref.".
currencycurrencyYes
itemsline_itemsYes
discountdiscountNo
tax_ratepercentNoTax on the subtotal after discount, on its own line.
tax_labelenumNoOne of "VAT", "GST", "Sales tax", "Tax". Name of the tax line; needs tax_rate. Without it the line is called Tax.
amount_paidmoneyNo
payment_termstextNo
payment_instructionsmultilineNo
payment_linkurlNoYour own payment link (PayPal.me, a Stripe Payment Link, a bank pay-me link), printed in full under "Pay online" as a clickable link. Koutei takes no payments.
notesmultilineNo

Rules across fields:

  • At least one line item. (no_items)
  • At most 50 line items. (too_many_items)
  • The subtotal (the sum of the rounded lines) must be at least 0. (negative_total)
  • The subtotal must be below 10^15. (total_too_large)
  • The discount may not exceed the subtotal. (discount_too_large)
  • amount_paid may not exceed the total. (amount_paid_too_large)
  • tax_label needs tax_rate. (requires_field)

What it is, and is not:

  • A seller-issued invoice with the heading INVOICE.
  • Totals are calculated from the line items: each amount is quantity × unit price, rounded to the currency’s minor unit.
  • Not a tax invoice: the heading is never TAX INVOICE.
  • Not checked for tax or legal compliance: Koutei never asserts that a document is tax-compliant or legally valid.
  • Not an e-invoice: it carries no Factur-X, ZUGFeRD or XRechnung data, and it is not PDF/A.
  • Not numbered by Koutei: you supply the document number, and it is never allocated or checked for uniqueness.
  • Not stored: neither the fields nor the PDF are kept, and nothing is sent to anyone.
{
  "template": "invoice",
  "template_version": "2",
  "mode": "final",
  "reference": "JOB-0017",
  "paper": "a4",
  "fields": {
    "seller_name": "Aiko Tanaka Design",
    "seller_details": "aiko@example.com\n12 Example Street, Cape Town",
    "tax_id": "4000000000",
    "client_name": "Northwind Pty Ltd",
    "client_details": "accounts@northwind.example",
    "invoice_number": "INV-0042",
    "date": "2026-10-12",
    "due_date": "2026-10-26",
    "po_number": "PO-7731",
    "related_document": "Q-0042",
    "currency": "ZAR",
    "items": [
      {
        "description": "Brand workshop",
        "quantity": 1,
        "unit_price": "6000.00"
      },
      {
        "description": "Design hours",
        "quantity": "7.5",
        "unit_price": "850.00"
      }
    ],
    "discount": "10%",
    "tax_rate": "15",
    "tax_label": "VAT",
    "amount_paid": "2000.00",
    "payment_terms": "Due within 14 days",
    "payment_instructions": "Bank: Example Bank · Account no.: 000000000\nReference: INV-0042"
  }
}

quote · Quote / Estimate, version 1.0.0

A quote or an estimate: the scope of work with either 1 to 50 line items and their total, or one fixed amount, plus optional terms and an acceptance block. Paper: "a4" or "letter".

Fields of the quote template
FieldTypeRequiredNotes
document_titleenumYesOne of "Quote", "Estimate".
seller_nametextYes
seller_detailsmultilineNo
tax_idtextNo
client_nametextYesPrinted under "Prepared for".
client_detailsmultilineNo
quote_numbertextYes
datedateYes
valid_untildateNo
related_documenttextNoPrinted "Ref.", e.g. the quote this one revises.
scopemultilineYes
currencycurrencyYes
itemsline_itemsNo
amountmoneyNoA fixed total instead of items; printed normalised (e.g. JPY 110,000).
termsmultilineNo
notesmultilineNo
show_acceptancebooleanNoAdds an "Accepted by" block. Omitted means false.

Rules across fields:

  • Exactly one of items (1 to 50) and amount. (items_or_amount)
  • An items list needs at least one line item. (no_items)
  • At most 50 line items. (too_many_items)

What it is, and is not:

  • A seller-issued quote or estimate; document_title chooses the heading.
  • Either line items with a calculated total, or a fixed amount. There is no tax arithmetic.
  • Not a contract: the acceptance block is a place to sign, nothing is signed electronically.
  • Not checked for tax or legal compliance: Koutei never asserts that a document is tax-compliant or legally valid.
  • Not an e-invoice: it carries no Factur-X, ZUGFeRD or XRechnung data, and it is not PDF/A.
  • Not numbered by Koutei: you supply the document number, and it is never allocated or checked for uniqueness.
  • Not stored: neither the fields nor the PDF are kept, and nothing is sent to anyone.
{
  "template": "quote",
  "mode": "final",
  "reference": "JOB-0020",
  "fields": {
    "document_title": "Estimate",
    "seller_name": "Kumo Studio",
    "seller_details": "hello@kumo.example",
    "client_name": "Sakura Cafe",
    "quote_number": "Q-0107",
    "date": "2026-10-12",
    "valid_until": "2026-11-11",
    "scope": "Menu photography and layout for the autumn menu.",
    "currency": "JPY",
    "items": "Photography (half day) | 1 | 60000\nMenu layout | 2 | 25,000",
    "terms": "50% deposit to start.",
    "show_acceptance": true
  }
}

receipt · Payment Receipt, version 1.0.0

A receipt a seller issues after a client has paid: who paid, the amount, the payment date and method, what it was for and the invoice it settles. Paper: "a4" or "letter".

Fields of the receipt template
FieldTypeRequiredNotes
seller_nametextYes
seller_detailsmultilineNo
tax_idtextNo
client_nametextYesPrinted under "Received from".
client_detailsmultilineNo
receipt_numbertextYes
payment_datedateYes
related_documenttextNoThe invoice this payment settles, printed "Invoice no.".
descriptionmultilineYes
currencycurrencyYes
amountmoneyYesMore than 0; printed normalised (e.g. KWD 1,250.500) under "Amount received".
payment_methodtextYes
payment_referencetextNo
notesmultilineNo

Older names, still accepted: invoice_reference (use related_document).

Rules across fields:

  • amount must be more than 0. (must_be_positive)
  • Send related_document or its deprecated alias invoice_reference, not both. (conflicting_fields)

What it is, and is not:

  • A seller-issued payment receipt with a PAID stamp under the amount received.
  • Not a till or point-of-sale receipt, and it carries no third-party branding.
  • Not checked for tax or legal compliance: Koutei never asserts that a document is tax-compliant or legally valid.
  • Not an e-invoice: it carries no Factur-X, ZUGFeRD or XRechnung data, and it is not PDF/A.
  • Not numbered by Koutei: you supply the document number, and it is never allocated or checked for uniqueness.
  • Not stored: neither the fields nor the PDF are kept, and nothing is sent to anyone.
{
  "template": "receipt",
  "mode": "final",
  "reference": "JOB-0021",
  "paper": "letter",
  "fields": {
    "seller_name": "Gulf Freight Brokers",
    "client_name": "Al Noor Trading",
    "receipt_number": "R-0051",
    "payment_date": "2026-10-14",
    "related_document": "INV-0051",
    "description": "Settlement of invoice INV-0051 (customs brokerage, October).",
    "currency": "KWD",
    "amount": "1250.5",
    "payment_method": "Bank transfer",
    "payment_reference": "TRF-88213"
  }
}

Value types

Value types
TypeRules
textOne line, at most 200 characters (Unicode code points), or a JSON integer (used as its digits). null or blank on an optional field leaves it off the document. Bidi controls (U+202A-U+202E, U+2066-U+2069), U+FEFF and U+200B are rejected (invalid_characters); U+200C-U+200F are allowed.
multilineAt most 2,000 characters. Line breaks are LF or CR LF; a CR not followed by LF is invalid_characters.
dateA real calendar date as YYYY-MM-DD, printed as sent.
enumOne of the values listed for the field, exact and case-sensitive.
booleantrue or false. Leaving it out means false.
currencyISO 4217 List One (published 2026-09-17) minus fund codes and codes without minor units. Capitals only; no symbols.
moneyA decimal of 0 or more with at most the currency's minor digits.
percent0 to 100, at most 4 fraction digits, optionally ending % (strings only).
discountA money amount ("50.00") or a percentage of the subtotal ending % ("10%").
line_items1 to 50 items: an array of line item objects, or a string with one "description | quantity | unit price" per non-blank line (at most 12,000 characters; the last two | parts are the numbers). Each amount is quantity x unit price rounded half away from zero to the currency's minor unit; the subtotal is the sum of the rounded amounts.
urlAn https:// link of at most 300 characters once leading and trailing spaces are trimmed, in plain ASCII with no spaces, no user name or password, and the host written plainly after https://. http:, javascript:, data:, mailto: and every other scheme are refused, as is any other breach of these rules (invalid_url). The document prints the trimmed text with the scheme in lower case, and links to exactly that text. null or blank on an optional field leaves it off the document. The schema states no pattern or length of its own, so a client that validates against it never refuses a padded or blank value that the server accepts; the server applies the rules.
decimal"1,250.00", "7.5", "-100" or a finite JSON number (read as its shortest round-trip decimal). Commas only as thousands separators; at most 10 integer and 4 fraction digits.

A line item

In the array form of line_items, each item is an object with these 3 names. An item with any other name is refused.

A line item
NameTypeWhat it is
descriptionstringWhat the line is for, on one line.
quantitystring | numberHow many. It may be negative, for a credit line.
unit_pricestring | numberThe price of one. It may be negative, for a credit line.
  • currency accepts 155 currency codes. The currency decides how many decimal places an amount may have and how it is printed.
  • A required field that is missing, null or blank is refused. An optional field that is missing, null or blank is left off the document, with its label.
  • Text is printed as you send it: characters that mean something in a markup language are printed as themselves.

The response

A document is answered with 201, whether it is new or a repeat. The body is the PDF, unless the Accept header lists application/json and none of application/pdf, application/* or */*. So curl, a browser and most HTTP tools get the file; only an explicit Accept: application/json gets JSON. Errors are always JSON.

Headers of a document

Headers of a document response
HeaderWhat it is
LocationThe path of the document’s record.
Content-DispositionBinary PDF form only; the JSON form carries the name in its body instead. RFC 6266: attachment; filename="<ASCII fallback>", plus filename*=UTF-8''<percent-encoded> when the name is not pure ASCII. The filename* value percent-encodes every byte outside RFC 8187 attr-char, including , ; ( ) [ ] and space.
Koutei-Document-IdStable document id; a replay returns the original id.
Koutei-Document-Sha256SHA-256 of the PDF bytes, lowercase hex.
Koutei-TemplateTemplate slug used.
Koutei-Template-VersionFull semantic version of the template used.
Koutei-ModeWhether the document is a final document or a preview. one of "final", "preview"
Koutei-Credit-UsedWhether this call used a credit (on 201 responses). one of "true", "false"
Koutei-Credits-RemainingCredit balance after this call.
Koutei-Idempotent-ReplayWhether this answer repeats a document made earlier with the same Idempotency-Key. one of "true", "false"
Koutei-ReferenceThe request's reference label, echoed when one was sent.
Koutei-Totals-Currencytotals.currency, the request's currency code. On every 201.
Koutei-Totals-Subtotaltotals.subtotal: sent when the document prints a subtotal (every invoice; a quote with items).
Koutei-Totals-Discounttotals.discount, never negative; sent when the invoice has a discount.
Koutei-Totals-Taxtotals.tax; sent when the invoice has a tax_rate.
Koutei-Totals-Totaltotals.total; sent for an invoice and a quote.
Koutei-Totals-Amount-Paidtotals.amount_paid, never negative; sent for an invoice with amount_paid, and for a receipt (the amount received).
Koutei-Totals-Balance-Duetotals.balance_due; sent for an invoice.
Koutei-VersionThe API version that served the request.
Koutei-Request-IdOpaque request id (also error.request_id). Quote it in support requests.
RateLimit-LimitQuota of the applicable bucket with the smallest remaining (a tie goes to the one that resets later), in its current window. requests applies to every request; final to new finals; the preview buckets to previews. Only your own quotas are reported (requests, final, the per-key and per-account preview buckets), never the shared preview ceiling or the failed-authentication counter; a response that ends before the mode's buckets are counted, a replay included, reports requests only.
RateLimit-RemainingRequests left in that window.
RateLimit-ResetSeconds until that window resets.

The file is named after the template and your document number, for example invoice-INV-0042.pdf; a preview ends -preview.pdf.

The JSON form

The JSON form of a 201 (Accept application/json only).

JSON form of a document response
NameTypeWhat it is
documentDocumentStored metadata of one document; the same object in every response.
filenamestringFull UTF-8 file name. Not stored.
file_base64stringThe PDF, base64 (at most 3,000,000 bytes before encoding).
credit_usedbooleanWhether this request used a credit. A repeat never does.
credits_remainingintegerThe credit balance after this request.
idempotent_replaybooleanWhether this answer repeats a document made earlier with the same Idempotency-Key.
totalsTotalsThe figures the document prints: see The totals.

The totals

The money figures the document prints, on every valid document request's answer: currency, then each figure the document prints, as a money string. A member the document does not print is absent, never null. invoice: subtotal, total and balance_due always; discount, tax and amount_paid when the request sent discount, tax_rate and amount_paid. quote: subtotal with items; total always. receipt: amount_paid (the amount received). The figures come from the same arithmetic as the document, run on the normalised request the document is rendered from; a replay states the same totals.

The totals object
NameTypeWhat it is
currencystringThe request's currency code.
subtotalstringA figure as the document prints it, without the currency code and the thousands separators: "." as the decimal point and exactly the currency's minor digits ("10808.13", JPY "110000", KWD "1250.500"). Only a quote with credit lines can be below zero. A figure can be sent back as a money value only when it is 0 or more with at most 10 digits before the point, and as the receipt's amount only when it is also more than 0.
discountstringThe discount, never negative (0 when sent as 0); the invoice prints it with a minus sign.
taxstringThe tax line, calculated on the subtotal after the discount.
totalstringThe subtotal less the discount, plus the tax (a quote's total is its subtotal or its fixed amount).
amount_paidstringWhat was paid, never negative: the invoice's amount_paid (printed as a plain figure, with Balance due under it), or the receipt's amount received.
balance_duestringThe total less the amount paid; the total when nothing was paid.
Totals each template states
Memberinvoicequotereceipt
currencyalwaysalwaysalways
subtotalalwayswith itemsnot stated
discountwith discountnot statednot stated
taxwith tax_ratenot statednot stated
totalalwaysalwaysnot stated
amount_paidwith amount_paidnot statedalways
balance_duealwaysnot statednot stated

On a 201, each member is also a header, present exactly when the member is: Koutei-Totals-Currency, Koutei-Totals-Subtotal, Koutei-Totals-Discount, Koutei-Totals-Tax, Koutei-Totals-Total, Koutei-Totals-Amount-Paid and Koutei-Totals-Balance-Due. The PDF form has no body to hold them, so the headers are where an automation reads the total due. A check (POST /api/v1/documents/validate) states the same totals before anything is made, and a repeat with the same Idempotency-Key states the same totals as the first answer. Errors have none.

The Document object

Stored metadata of one document; the same object in every response. The values you sent in fields are never in it.

The Document object
NameTypeWhat it is
idstringThe document’s id. A repeat of the same request returns the same id.
object"document"What kind of object this is.
templatestringThe template used.
template_versionstringThe full version of the template used.
mode"final" | "preview"Whether it is a final document or a preview.
paper"a4" | "letter"The page size used.
localestringThe language used.
referencestring | nullYour label, or null if you sent none.
sourcestringHow the document was made, such as "api". New values may be added.
credits_consumed0 | 1Credits spent when this document was created: 1 for a final, 0 for a preview. A replay does not change it. The call-level credit_used is separate.
sha256stringThe SHA-256 fingerprint of the PDF, in lower-case hex.
bytesintegerThe size of the PDF in bytes.
content_type"application/pdf"The type of the file.
created_atstring (date-time)When the document was first made.
file_urlnullReserved; always null in 2026-10-01.

Other answers

POST /api/v1/documents/validate:

Answer of the validate route
NameTypeWhat it is
validtrueThe request would make a document.
templatestringThe template the request names.
template_versionstringThe version of the template that would be used.
totalsTotalsThe figures the document would print: see The totals.
credit_usedfalseA check never uses a credit.

GET /api/v1/documents: Ordered by created_at, then id, both descending. A starting_after that is not one of your listable documents gets 400 invalid_request (invalid_query).

Answer of the documents list
NameTypeWhat it is
object"list"What kind of object this is.
data[]DocumentThe records, newest first.
has_morebooleanWhether there are older records. Pass the last id as starting_after to get them.

GET /api/v1/account:

Answer of the account route
NameTypeWhat it is
object"account"What kind of object this is.
planstringfree, starter or pro (an open set).
creditsobjectThe account’s credits.
credits.remainingintegerCredits left in the current period.
credits.monthly_allowanceintegerCredits the plan gives each month.
credits.period_endstring | nullWhen the current credit period ends, or null if there is none.
keyobjectThe key that made this request. Never its secret.
key.idstringThe key’s id.
key.namestringThe name you gave the key.
key.prefixstringThe first characters of the key, as Settings shows them.
key.scopes[]"documents:create" | "documents:preview" | "documents:read" | "account:read"What the key may do.
key.created_atstring (date-time)When the key was created.
key.last_used_atstring | nullWhen the key was last used, or null.
limitsobjectCurrent operational values (not frozen; new names may be added). _key values apply to this key alone; _account values are shared by every key and the web app.
limits.final_per_minute_keyintegerNew final documents this key may make each minute.
limits.requests_per_minute_keyintegerRequests this key may send each minute.
limits.preview_per_hour_keyintegerPreviews this key may make each hour.
limits.preview_per_day_keyintegerPreviews this key may make each day.
limits.preview_per_hour_accountintegerPreviews the whole account may make each hour.
limits.preview_per_day_accountintegerPreviews the whole account may make each day.

GET /api/v1/templates returns a list of these; GET /api/v1/templates/{slug} returns one, with the detail rows:

A template
NameTypeWhat it is
object"template"What kind of object this is.
slugstringThe name to send as template.
namestringThe template’s name in words.
descriptionstringWhat the document is.
categorystringThe group the template belongs to.
versionstringThe template’s current version.
output"pdf"The kind of file it makes.
papers[]"a4" | "letter"The page sizes it can be made on.
locales[]stringThe languages it can be made in.
required_fields[]stringThe fields a request must send.
optional_fields[]stringThe fields a request may send.
fields_schemaobjectJSON Schema 2020-12 of the template's fields.
rules[]objectThe rules that involve more than one field.
rules[].codestringThe detail code a broken rule answers with.
rules[].descriptionstringThe rule, in words.
exampleobjectA complete, valid request body.
notesobjectWhat the document is, and what it is not.
notes.is[]stringWhat it is.
notes.is_not[]stringWhat it is not.
deprecated_fields[]objectOlder field names that are still accepted.
deprecated_fields[].namestringThe older name.
deprecated_fields[].use_insteadstringThe name to send instead.

Idempotency

  • Send an Idempotency-Key header with every request for a final document. Without one, every successful request makes a new document and uses a credit, including one you send twice by mistake.
  • A key is remembered for 24 hours, for the whole account. The same key with the same request returns the same document, free: the document is made again, checked against its recorded fingerprint, and answered with Koutei-Idempotent-Replay: true.
  • The same key with a different request is refused with 422 idempotency_key_reused. A different reference counts as a different request; a different Accept header does not.
  • If the first request is still running, the answer is 409 idempotency_in_progress with Retry-After: 5. A request that has been running for more than 35 seconds is treated as dead and can be taken over. At most one document is ever charged for one key.
  • If the template has changed since, the document cannot be made byte for byte again, and the answer is 409 idempotency_replay_unavailable. You never get different bytes under an old document id.
  • POST /api/v1/documents/validate ignores the header.

Credits and previews

  • A final document uses 1 credit from the same monthly allowance as the app. The credit is taken only after the PDF has been made, and the PDF is sent only after the credit is recorded.
  • A preview, a validation, a repeat with the same Idempotency-Key and a request that fails before the PDF is made use no credit. A preview works with 0 credits.
  • Every page of a preview carries the words "PREVIEW · NOT A VALID DOCUMENT", its PDF title is "Preview – not a valid document", and its file name ends -preview.pdf. A final document never has the watermark.
  • With no credit left, a final request answers 402 insufficient_credits, with the balance and the end of the current credit period.
  • Every JSON answer to a document request states credit_used: true, false, or null when the outcome is unknown.
  • Need more than 10,000 documents a month? Email support@kouteisystems.com.

Limits

Rate limits count in fixed windows on the UTC clock: a minute on the minute, an hour on the hour, a day from 00:00 UTC. A request over a limit is refused with a Retry-After header. These are the launch values, and they can change.

The RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers report one limit: of the limits that apply to the request and are marked Yes in the last column, the one you are closest to. That is 4 of the 8 below: your own key's and your own account's. The headers never report a limit marked No.

Rate limits
What is countedFreeStarterProRefused withIn the headers
New final documents, per key. Replays are not counted.10 per minute60 per minute120 per minute429 rate_limited (final_key_limit)Yes
Every authenticated request, per key. Replays are counted.120 per minutesamesame429 rate_limited (request_key_limit)Yes
Previews, per account: every key and the web app together.60 per hour and 300 per daysamesame429 rate_limited (preview_user_limit)Yes
Previews, per key.30 per hour and 200 per daysamesame429 rate_limited (preview_key_limit)Yes
Previews, for everyone together.1,500 per hour and 10,000 per daysamesame503 service_busy (preview_global_limit)No
Failed sign-ins (a missing or wrong key), per network.20 per 10 minutessamesame429 rate_limited (auth_failure_limit)No
Idempotent replays, per account: every key together. A replay renders the document again; it uses no credit.finals: 120 per minute and 600 per hour; previews: 60 per hour and 300 per dayfinals: 120 per minute and 1,200 per hour; previews: 60 per hour and 300 per dayfinals: 120 per minute and 3,000 per hour; previews: 60 per hour and 300 per day429 rate_limited (replay_limit)No
Idempotent replays, per key.finals: 60 per minute; previews: 30 per hour and 200 per daysamesame429 rate_limited (replay_limit)No

These limits are part of the API version and do not change within it:

Size limits
LimitValue
Request body128 KiB (131,072 bytes)
Names in fields64
A text value200 characters
A multiline value2,000 characters
Line items, at least1
Line items, at most50
A line item description200 characters
Line items sent as one text value12,000 characters
A url value (the invoice’s payment link)300 characters
Digits before the decimal point of a number10
Digits after the decimal point of a number4
The reference label128 characters
The Idempotency-Key header255 characters
How long an Idempotency-Key is remembered24 hours
The PDF, sent as a file4,000,000 bytes
The PDF, sent inside JSON3,000,000 bytes, before base64
How long a document stays in your history90 days

Versions

  • The API version is a date, 2026-10-01, sent as the Koutei-Version header. It is the only version today. Without the header you get the newest version; a version the API does not have is refused with 400 invalid_request.
  • What can change within a version: only additions. New routes, headers, response fields, error codes, templates, optional parameters and accepted values may appear under a new version date. Write your code to ignore response fields and headers it does not know, and to treat an unknown error code by its HTTP status.
  • What cannot: nothing is removed or renamed, nothing optional becomes required, input that was accepted is not refused, and no status code, default or meaning changes. A change of that kind would be a new API, under /api/v2.
  • Rate limits are not part of a version. The values in the first table above can change.
  • Each template has its own version, such as 2.0.0, returned in Koutei-Template-Version. A new first number means its fields changed in a way that could break a request. A new second number may add an optional field or change how the document looks. A new third number changes nothing in the output.
  • Pinning. Send template_version with the first number, such as "2", to hold a template at that major version. If the current major is a different one, the answer is 409 template_version_unavailable, never a different document.
  • With one template version and one request, the PDF is the same bytes every time.

What is kept

  • The values you send in fields and the PDF are never stored. The PDF exists only in the response.
  • A record of each document is kept for 90 days: the Document object above, and the key that made it. GET /api/v1/documents lists these records.
  • Don't put personal information in reference: it is stored with the record and appears in request addresses when you filter by it.
  • The Privacy Policy has the full list.

Errors

An error is JSON with a code, a message, sometimes details, a request_id and a docs_url. Message texts may change; codes do not. Errors explains each code.

Error codes
StatusCodecredit_used
400invalid_requestfalse
400reserved_fieldfalse
401unauthorizedfalse
402insufficient_creditsfalse
403forbidden_scopefalse
404not_foundfalse
404template_not_foundfalse
404document_not_foundnot stated (this is not a document request)
404file_not_availablenot stated (this is not a document request)
405method_not_allowedfalse
409template_version_unavailablefalse
409idempotency_in_progressfalse
409idempotency_replay_unavailablefalse
413payload_too_largefalse
413response_too_largefalse
415unsupported_media_typefalse
422validation_failedfalse
422idempotency_key_reusedfalse
422render_rejectedfalse
429rate_limitedfalse
500commit_status_unknownnull (unknown: retry with the same Idempotency-Key)
500internal_errorfalse, or null (unknown: retry with the same Idempotency-Key)
502engine_unavailablefalse
503engine_busyfalse
503service_busyfalse
504render_timeoutfalse

Good to know

  • details lists at most 50 problems. The message counts all of them.
  • Send amounts and quantities as strings when the exact digits matter. A JSON number is read as the shortest decimal that gives the same number, so 7.50 is read as 7.5.
  • In a line item object, a number sent as a string must have no spaces around it. In the one-text form of line items, the spaces around each | are ignored.
  • A 429 that is not JSON comes from the firewall in front of the API, not from the API itself. Wait a minute and try again.

The API does not send documents to anyone, does not allocate or check document numbers, and does not state that a document is tax-compliant, an e-invoice or legally valid.