API quickstart
Send the details of a document as JSON with an API key, and get the PDF back in the same response. This page takes you from a new key to a finished invoice in five steps.
Before you start
- You need a Koutei account with a confirmed email address, and a terminal with curl.
- The API makes 3 kinds of document: invoice, quote / estimate, payment receipt. This page makes an invoice.
- Call the API from a server or a script, never from a web page. A key in a browser can be copied by anyone who opens the page.
What this API won't do (yet)
- 3 templates: invoice, quote and receipt. The website has 7; the others are not in the API, and you can't send your own layout.
- Labels print in English only ("Invoice", "Bill to", "Total"). Your own details print as you send them.
- No stored files: Koutei keeps no copy, so save the PDF from the answer. The same request with the same Idempotency-Key within 24 hours returns it again, free.
- No logo yet: a request with a logo is refused.
- No webhooks or callbacks: the PDF comes back in the answer to the request that asked for it.
1. Create a key
Sign in, open Settings and find API keys. Give the key a name, choose Full access and create it. The key is shown once, so copy it now. It starts with kt_live_. You can have up to 3 active keys.
Put the key in your shell, so the commands below can use it:
export KOUTEI_API_KEY="kt_live_…"A Preview only key can check requests and make previews, and can never use a credit. It is a good first key for something you are still testing. Step 5 needs a Full access key.
2. Write the request
Save this as invoice.json:
{
"template": "invoice",
"mode": "preview",
"reference": "JOB-0017",
"fields": {
"seller_name": "Aiko Tanaka Design",
"client_name": "Northwind Pty Ltd",
"invoice_number": "INV-0042",
"date": "2026-10-12",
"currency": "ZAR",
"items": [
{
"description": "Brand workshop",
"quantity": 1,
"unit_price": "6000.00"
},
{
"description": "Design hours",
"quantity": "7.5",
"unit_price": "850.00"
}
]
}
}templateis the kind of document.fieldsholds its details. The API reference lists every field of every template.modeisprevieworfinal. There is no default: a request never uses a credit unless it saysfinal.referenceis your own label for the document, such as a job number. It is never printed. It is stored with the document's record, so don't put personal information in it.- You supply the document number and the date. The API never allocates a number, never checks one for uniqueness, and never fills in today's date.
3. Check it
Ask the API whether the request would make a document. Nothing is made, and no credit is used.
curl -sS -X POST "https://kouteisystems.com/api/v1/documents/validate" \
-H "Authorization: Bearer $KOUTEI_API_KEY" \
-H "Koutei-Version: 2026-10-01" \
-H "Content-Type: application/json" \
--data-binary @invoice.jsonIf the request is good, the answer is:
{
"valid": true,
"template": "invoice",
"template_version": "2.0.0",
"totals": {
"currency": "ZAR",
"subtotal": "12375.00",
"total": "12375.00",
"balance_due": "12375.00"
},
"credit_used": false
}If it is not, the answer is 422 validation_failed with every problem listed at once: the field, a code and a message for each.
4. Make a preview
Send the same file to the documents route. The PDF comes back as the response body.
curl -sS -X POST "https://kouteisystems.com/api/v1/documents" \
-H "Authorization: Bearer $KOUTEI_API_KEY" \
-H "Koutei-Version: 2026-10-01" \
-H "Content-Type: application/json" \
--data-binary @invoice.json \
-D - \
-o invoice-INV-0042-preview.pdf-o saves the PDF as invoice-INV-0042-preview.pdf and -D - prints the response headers. A preview is free. Every page carries the words "PREVIEW · NOT A VALID DOCUMENT", so it is for checking the layout, not for sending to anyone.
5. Make the final document
In invoice.json, change "mode" to "final". Then send it with an Idempotency-Key header:
curl -sS -X POST "https://kouteisystems.com/api/v1/documents" \
-H "Authorization: Bearer $KOUTEI_API_KEY" \
-H "Koutei-Version: 2026-10-01" \
-H "Idempotency-Key: JOB-0017-invoice" \
-H "Content-Type: application/json" \
--data-binary @invoice.json \
-D - \
-o invoice-INV-0042.pdfThe invoice is saved as invoice-INV-0042.pdf, without a watermark. This uses one credit.
Keep the file. Koutei does not store documents or the details you send, so it cannot give you the PDF again later.
Read the receipt
The response headers are the receipt for the document. For the final invoice above, on a Free account with all 50 of its monthly credits, they include:
| Header | Example | What it tells you |
|---|---|---|
Koutei-Document-Id | doc_… | Stable document id; a replay returns the original id. |
Koutei-Document-Sha256 | 64 hex characters | SHA-256 of the PDF bytes, lowercase hex. |
Koutei-Template | invoice | Template slug used. |
Koutei-Template-Version | 2.0.0 | Full semantic version of the template used. |
Koutei-Mode | final | Whether the document is a final document or a preview. |
Koutei-Credit-Used | true | Whether this call used a credit (on 201 responses). |
Koutei-Credits-Remaining | 49 | Credit balance after this call. |
Koutei-Idempotent-Replay | false | Whether this answer repeats a document made earlier with the same Idempotency-Key. |
Koutei-Reference | JOB-0017 | The request's reference label, echoed when one was sent. |
Koutei-Totals-Currency | ZAR | totals.currency, the request's currency code. On every 201. |
Koutei-Totals-Subtotal | 12375.00 | totals.subtotal: sent when the document prints a subtotal (every invoice; a quote with items). |
Koutei-Totals-Total | 12375.00 | totals.total; sent for an invoice and a quote. |
Koutei-Totals-Balance-Due | 12375.00 | totals.balance_due; sent for an invoice. |
To get JSON instead of the file, send Accept: application/json. The body then holds document, filename, file_base64, credit_used, credits_remaining, idempotent_replay and totals, with the PDF in base64.
curl -sS -X POST "https://kouteisystems.com/api/v1/documents" \
-H "Authorization: Bearer $KOUTEI_API_KEY" \
-H "Koutei-Version: 2026-10-01" \
-H "Idempotency-Key: JOB-0017-invoice" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary @invoice.jsonThis command has the same Idempotency-Key as the one in step 5. If you run it after that one, it returns the same document and uses no credit.
In Node.js, Python or PHP
The same three steps (check, preview, final) as one script each. Every script reads the key from KOUTEI_API_KEY, as the commands above do, prints the totals the check returns, and saves both PDFs.
Save it as koutei.mjs and run node koutei.mjs:
// Node.js 18 or later: fetch is built in. Save as koutei.mjs and run: node koutei.mjs
import { writeFile } from 'node:fs/promises'
const BASE_URL = 'https://kouteisystems.com'
const HEADERS = {
Authorization: `Bearer ${process.env.KOUTEI_API_KEY}`,
'Koutei-Version': '2026-10-01',
'Content-Type': 'application/json',
}
const request = {
template: 'invoice',
mode: 'preview',
reference: 'JOB-0017',
fields: {
seller_name: 'Aiko Tanaka Design',
client_name: 'Northwind Pty Ltd',
invoice_number: 'INV-0042',
date: '2026-10-12',
currency: 'ZAR',
items: [
{
description: 'Brand workshop',
quantity: 1,
unit_price: '6000.00',
},
{
description: 'Design hours',
quantity: '7.5',
unit_price: '850.00',
},
],
},
}
async function post(path, body, extraHeaders = {}) {
const response = await fetch(BASE_URL + path, {
method: 'POST',
headers: { ...HEADERS, ...extraHeaders },
body: JSON.stringify(body),
})
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`)
return response
}
// 1. Check the request. Nothing is made and no credit is used.
const check = await (await post('/api/v1/documents/validate', request)).json()
console.log('Totals:', check.totals)
// 2. Make a preview: free and watermarked.
const preview = await post('/api/v1/documents', request)
await writeFile('invoice-INV-0042-preview.pdf', Buffer.from(await preview.arrayBuffer()))
// 3. Make the final document: 1 credit. Sent again with the same Idempotency-Key, it is free.
const final = await post('/api/v1/documents', { ...request, mode: 'final' }, { 'Idempotency-Key': 'JOB-0017-invoice' })
await writeFile('invoice-INV-0042.pdf', Buffer.from(await final.arrayBuffer()))
console.log('Credit used:', final.headers.get('Koutei-Credit-Used'), '| Balance due:', final.headers.get('Koutei-Totals-Balance-Due'))Save it as koutei.py and run python3 koutei.py:
# Python 3 with requests (pip install requests). Save as koutei.py and run: python3 koutei.py
import os
import requests
BASE_URL = "https://kouteisystems.com"
HEADERS = {
"Authorization": "Bearer " + os.environ["KOUTEI_API_KEY"],
"Koutei-Version": "2026-10-01",
}
request = {
"template": "invoice",
"mode": "preview",
"reference": "JOB-0017",
"fields": {
"seller_name": "Aiko Tanaka Design",
"client_name": "Northwind Pty Ltd",
"invoice_number": "INV-0042",
"date": "2026-10-12",
"currency": "ZAR",
"items": [
{
"description": "Brand workshop",
"quantity": 1,
"unit_price": "6000.00",
},
{
"description": "Design hours",
"quantity": "7.5",
"unit_price": "850.00",
},
],
},
}
def post(path, body, extra_headers=None):
response = requests.post(BASE_URL + path, json=body, headers={**HEADERS, **(extra_headers or {})}, timeout=60)
if not response.ok:
raise SystemExit(f"{response.status_code}: {response.text}")
return response
# 1. Check the request. Nothing is made and no credit is used.
print("Totals:", post("/api/v1/documents/validate", request).json()["totals"])
# 2. Make a preview: free and watermarked.
with open("invoice-INV-0042-preview.pdf", "wb") as file:
file.write(post("/api/v1/documents", request).content)
# 3. Make the final document: 1 credit. Sent again with the same Idempotency-Key, it is free.
final = post("/api/v1/documents", {**request, "mode": "final"}, {"Idempotency-Key": "JOB-0017-invoice"})
with open("invoice-INV-0042.pdf", "wb") as file:
file.write(final.content)
print("Credit used:", final.headers["Koutei-Credit-Used"], "| Balance due:", final.headers["Koutei-Totals-Balance-Due"])Save it as koutei.php and run php koutei.php:
<?php
// PHP 8 with the curl extension. Save as koutei.php and run: php koutei.php
const BASE_URL = 'https://kouteisystems.com';
$request = [
'template' => 'invoice',
'mode' => 'preview',
'reference' => 'JOB-0017',
'fields' => [
'seller_name' => 'Aiko Tanaka Design',
'client_name' => 'Northwind Pty Ltd',
'invoice_number' => 'INV-0042',
'date' => '2026-10-12',
'currency' => 'ZAR',
'items' => [
[
'description' => 'Brand workshop',
'quantity' => 1,
'unit_price' => '6000.00',
],
[
'description' => 'Design hours',
'quantity' => '7.5',
'unit_price' => '850.00',
],
],
],
];
function post(string $path, array $body, array $extraHeaders = []): array
{
$headers = [];
$curl = curl_init(BASE_URL . $path);
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => array_merge([
'Authorization: Bearer ' . getenv('KOUTEI_API_KEY'),
'Koutei-Version: 2026-10-01',
'Content-Type: application/json',
], $extraHeaders),
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
CURLOPT_HEADERFUNCTION => function ($curl, $line) use (&$headers) {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$headers[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
$answer = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if ($answer === false || $status >= 400) {
fwrite(STDERR, $status . ': ' . ($answer === false ? curl_error($curl) : $answer) . PHP_EOL);
exit(1);
}
return [$answer, $headers];
}
// 1. Check the request. Nothing is made and no credit is used.
[$answer] = post('/api/v1/documents/validate', $request);
echo 'Totals: ' . json_encode(json_decode($answer, true)['totals']) . PHP_EOL;
// 2. Make a preview: free and watermarked.
[$pdf] = post('/api/v1/documents', $request);
file_put_contents('invoice-INV-0042-preview.pdf', $pdf);
// 3. Make the final document: 1 credit. Sent again with the same Idempotency-Key, it is free.
[$pdf, $headers] = post('/api/v1/documents', array_merge($request, ['mode' => 'final']), ['Idempotency-Key: JOB-0017-invoice']);
file_put_contents('invoice-INV-0042.pdf', $pdf);
echo 'Credit used: ' . $headers['koutei-credit-used'] . ' | Balance due: ' . $headers['koutei-totals-balance-due'] . PHP_EOL;You can also generate a client from the OpenAPI document, or import it into a tool such as Postman.
Retrying safely
Send an Idempotency-Key with every final request: if you send the same request again with the same key within 24 hours, no second credit is used and you get the same document back. The same key with a different request is refused with 422 idempotency_key_reused, so a changed document needs a new key. The API reference has the details.
What it costs
- 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.
- A preview, a check (step 3) and a repeat of a request with the same Idempotency-Key use no credit.
- A request that is refused, or that fails before the PDF is made, uses no credit. Every answer to a document request says whether a credit was used:
credit_usedin a JSON body,Koutei-Credit-Usedon a PDF. - If an error says
credit_used: null, we could not confirm it either way. Wait 35 seconds and send the same request with the same Idempotency-Key: you are charged once at most. - The Free plan has 50 credits a month. Pricing lists the other plans.
When something goes wrong
An error is always JSON, with a code, a message and a docs_url that opens the explanation of that code. Errors explains every code and what to do about it. The API reference covers every route, field and limit.