Invoice
Itemised invoice with tax, discount, payment details and multi-page item tables.
REST · JSON in, PDF out · OpenAPI 3.1
Send a template id and your data, get a print-ready PDF back in the same response. Nothing you send is stored. Every endpoint, field and error is also in the API reference (OpenAPI 3.1 spec).
At launch: create a free account, confirm your email, then create a key in the dashboard. (Launching soon.) Keys look like pdfk_live_…. Keep them on your server.
KTAVLY_API_KEY (your key) and, for local testing, KTAVLY_API_URL=http://localhost:8787. The default is https://api.ktavly.com.
The response body is the PDF. Save it, attach it to an email, or stream it to your user.
# Render the built-in invoice template and save the PDF.
# On an error, curl exits non-zero and invoice.pdf holds the JSON error body instead (cat it to read why).
curl -sS "${KTAVLY_API_URL:-https://api.ktavly.com}/v1/render" \
-H "Authorization: Bearer $KTAVLY_API_KEY" \
-H "Content-Type: application/json" \
--fail-with-body -o invoice.pdf \
-d '{
"template": "invoice",
"data": {
"number": "INV-1001", "issueDate": "2026-10-18", "currency": "USD",
"from": { "name": "Northwind Studio" },
"to": { "name": "Lumen Analytics" },
"items": [{ "description": "Design sprint", "quantity": 1, "unitPrice": 6400 }],
"taxRate": 8.5
}
}'// Node 18+ (built-in fetch). Run: node quickstart.mjs
import { writeFile } from 'node:fs/promises';
const API = process.env.KTAVLY_API_URL ?? 'https://api.ktavly.com';
const res = await fetch(`${API}/v1/render`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.KTAVLY_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
template: 'invoice-he',
data: {
documentType: 'tax_invoice', number: '20261042', issueDate: '2026-10-18',
seller: { name: 'סטודיו גפן בע״מ', businessId: '514238765' },
customer: { name: 'אורן מערכות מידע בע״מ' },
items: [{ description: 'עיצוב ממשק משתמש', quantity: 1, unitPrice: 18500 }],
},
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
await writeFile('invoice-he.pdf', Buffer.from(await res.arrayBuffer()));
console.log(`Saved invoice-he.pdf (${res.headers.get('x-pages')} page, ${res.headers.get('x-usage-remaining')} left this month)`);# Python 3.8+ with requests (pip install requests). Run: python quickstart.py
import os
import requests
API = os.environ.get("KTAVLY_API_URL", "https://api.ktavly.com")
r = requests.post(
f"{API}/v1/render",
headers={"Authorization": f"Bearer {os.environ['KTAVLY_API_KEY']}"},
json={
"template": "receipt",
"data": {
"number": "RC-58213", "date": "2026-10-18", "currency": "EUR",
"business": {"name": "Café Lisboa"},
"customer": {"name": "Dana Kowalski"},
"items": [{"name": "Barista workshop", "quantity": 2, "price": 45}],
"taxRate": 23,
"payment": {"method": "Visa", "last4": "4242"},
},
"options": {"filename": "receipt-RC-58213.pdf"},
},
timeout=60,
)
if r.status_code != 200:
err = r.json()["error"]
raise SystemExit(f"{err['code']}: {err['message']}")
with open("receipt.pdf", "wb") as f:
f.write(r.content)
print(f"Saved receipt.pdf ({r.headers['X-Pages']} page)")<?php
// PHP 7.4+ with the curl extension. Run: php quickstart.php
$api = getenv('KTAVLY_API_URL') ?: 'https://api.ktavly.com';
$payload = [
'template' => 'report',
'data' => [
'title' => 'Q3 2026 Revenue Review',
'kpis' => [['label' => 'Revenue', 'value' => '$2.84M', 'delta' => 14.2]],
'sections' => [['heading' => 'Outlook', 'paragraphs' => ['Q4 revenue is expected to grow.']]],
],
];
$ch = curl_init("$api/v1/render");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('KTAVLY_API_KEY'), 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
$err = json_decode($body, true)['error'] ?? ['code' => 'network_error', 'message' => 'request failed'];
fwrite(STDERR, "{$err['code']}: {$err['message']}\n");
exit(1);
}
file_put_contents('report.pdf', $body);
echo "Saved report.pdf\n";// Node 18+ with the Ktavly SDK (npm install ktavly, at launch). Run: node sdk-quickstart.mjs
import { writeFile } from 'node:fs/promises';
import { Ktavly } from 'ktavly';
const ktavly = new Ktavly({
apiKey: process.env.KTAVLY_API_KEY,
baseUrl: process.env.KTAVLY_API_URL ?? 'https://api.ktavly.com',
});
// Typed errors (ValidationError, QuotaExceededError, RateLimitError, ...); 429/5xx are retried for you.
const { pdf, meta } = await ktavly.renderWithMeta('invoice-he', {
documentType: 'tax_invoice', number: '20261042', issueDate: '2026-10-18',
seller: { name: 'סטודיו גפן בע״מ', businessId: '514238765' },
customer: { name: 'אורן מערכות מידע בע״מ' },
items: [{ description: 'עיצוב ממשק משתמש', quantity: 1, unitPrice: 18500 }],
});
await writeFile('invoice-he.pdf', pdf);
console.log(`Saved invoice-he.pdf (${meta.pages} page, ${meta.usage.remaining} left this month)`);# Python 3.9+ with the Ktavly SDK (pip install ktavly, at launch). Run: python sdk-quickstart.py
import os
from ktavly import Ktavly
ktavly = Ktavly(
api_key=os.environ["KTAVLY_API_KEY"],
base_url=os.environ.get("KTAVLY_API_URL", "https://api.ktavly.com"),
)
# Typed errors (ValidationError, QuotaExceededError, RateLimitError, ...); 429/5xx are retried for you.
result = ktavly.render_with_meta(
"receipt",
{
"number": "RC-58213", "date": "2026-10-18", "currency": "EUR",
"business": {"name": "Café Lisboa"},
"customer": {"name": "Dana Kowalski"},
"items": [{"name": "Barista workshop", "quantity": 2, "price": 45}],
"taxRate": 23,
"payment": {"method": "Visa", "last4": "4242"},
},
{"filename": "receipt-RC-58213.pdf"},
)
result.save("receipt.pdf")
print(f"Saved receipt.pdf ({result.meta.pages} page)")The first four tabs are raw HTTP; the SDK tabs make the same calls through the official clients. Every file lives in snippets/ and the test suite runs it against a local server, so what you copy here is what we test.
Typed clients for Node.js / TypeScript (ktavly: ESM + CommonJS, zero dependencies, built on fetch) and Python (ktavly: sync Ktavly and async AsyncKtavly on httpx, fully typed). They are ready but not on npm or PyPI yet; they will be published at launch.
render(template, data, options) returns the PDF bytes; renderWithMeta / render_with_meta adds pages, usage and request id; renderStream / render_stream streams it to disk or to your HTTP response.templates.list(), templates.sample(id) and usage().AuthenticationError 401, QuotaExceededError 402, PermissionDeniedError 403, PayloadTooLargeError 413, UnsupportedMediaTypeError 415, ValidationError 422 (with every problem), RateLimitError 429 (with retryAfter).Retry-After), and your key is never logged.new Ktavly({ demo: true }) / Ktavly(demo=True) renders built-in templates through the public demo.# Raw HTML → PDF with page numbers and a Hebrew line
curl "$KTAVLY_API_URL/v1/render" \
-H "Authorization: Bearer $KTAVLY_API_KEY" -H "Content-Type: application/json" \
-d '{"html":"<h1>Hello</h1><p>שלום עולם</p>",
"options":{"format":"A4","margin":"15mm","pageNumbers":true,"footer":"Acme Ltd · confidential"}}' \
-o hello.pdf
# Same request, but get JSON with base64 (handy for Zapier / Make)
curl "$KTAVLY_API_URL/v1/render" -H "Accept: application/json" ...
# Any template with its own sample data
curl -s "$KTAVLY_API_URL/v1/templates/invoice-he/sample" | \
jq '{template:"invoice-he", data:.}' | \
curl "$KTAVLY_API_URL/v1/render" -H "Authorization: Bearer $KTAVLY_API_KEY" \
-H "Content-Type: application/json" -d @- -o invoice-he.pdfPOST /v1/render| Field | Type | Description |
|---|---|---|
template | string | object | A built-in template id, or an inline template exported from the editor: {"base":"invoice","styles":{…},"labels":{…},"page":{…}}. Advanced: {"html":"…handlebars…","css":"…","schema":{…}}. |
data | object | Your document data. Validated against the template's JSON Schema; errors come back as 422 validation_failed with a list of problems. |
html | string | Instead of a template: a full HTML document or a fragment. Fonts and print CSS are injected for you. JavaScript does not run. |
styles | object | primaryColor, accentColor, textColor (#RRGGBB), fontFamily, headingFont, direction (ltr/rtl/auto), locale (e.g. he-IL; drives number, currency and date formats), logoUrl (https or base64 data URL), accentStyle, density. |
options | object | Page setup, see below. |
| Option | Example | Notes |
|---|---|---|
format | "A4" | A3, A4, A5, Letter, Legal, Tabloid. Or set width + height ("100mm", "4in", "800px"). |
landscape | true | Rotate the page. |
margin | "12mm" or {"top":"20mm","bottom":"18mm"} | Units: mm, cm, in, px. |
header / footer | "Acme · {page}/{pages}" | Text (escaped) or {"html":"…","align":"center"}. Placeholders: {page}, {pages}, {title}. Repeated on every page, RTL-aware. |
pageNumbers | {"position":"footer","align":"center","format":"{page} / {pages}"} | true/false or an object. Defaults to “Page 1 of 3” (or “עמוד 1 מתוך 3” for RTL). |
fonts | ["heebo","frank-ruhl-libre"] | Extra embedded fonts for raw HTML: inter, heebo, frank-ruhl-libre, playfair-display, noto-sans-arabic, jetbrains-mono. |
filename | "invoice-1001.pdf" | Sets Content-Disposition. Add ?download=1 to the URL for attachment. |
allowRemoteAssets | true | Off by default. When on, public https images, fonts and stylesheets may load. Private and internal addresses are always blocked. |
Long tables split across pages cleanly: table headers repeat on every page and rows never break in half. Add class="page-break" to force a new page, or class="keep" to keep a block together.
Each template ships with a JSON Schema and sample data (see the gallery for previews and sample PDFs). GET /v1/templates lists them; /v1/templates/{id}/schema and /v1/templates/{id}/sample?lang=he return the parts.
Itemised invoice with tax, discount, payment details and multi-page item tables.
חשבונית מס / קבלה: RTL layout, ע.מ / ח.פ, allocation number, VAT and payments.
Payment receipt (A5) with paid amount, method, line items and VAT breakdown.
Cover summary, KPI cards, bar chart and a long data table whose header repeats on every page.
A4 landscape certificate with ornamental border, seal and signatures.
Set styles.locale to he-IL or ar (or just send Hebrew/Arabic data with direction: "auto") and the whole document flips: layout, table columns, headers and footers, page numbers. Heebo, Frank Ruhl Libre and Noto Sans Arabic are embedded in every PDF, so text never falls back to boxes. Numbers, amounts and IDs stay left-to-right inside RTL text.
{"template":"invoice-he","data":{"documentType":"tax_invoice_receipt","number":"20261042", …}}
// or any template in Hebrew:
{"template":"invoice","styles":{"locale":"he-IL"},"data":{"currency":"ILS","number":"חש-0087", …}}200 with Content-Type: application/pdf. Send Accept: application/json to get {"filename","pages","bytes","base64"} instead.
| Header | Meaning |
|---|---|
X-Pages | Number of pages in the PDF. |
X-Usage-Used / X-Usage-Limit / X-Usage-Remaining | Your account's documents this month (UTC calendar month). |
X-Usage-Overage | true when this document was billed as overage. |
X-RateLimit-Limit / X-RateLimit-Remaining | Requests per minute for your plan. |
X-Request-Id | Quote this when contacting support. We can look up timing and status, never content. |
GET /v1/usage returns your plan, documents used and remaining this month, overage, per-key usage and a 12-month history. The usage dashboard shows the same data. Only successful renders count.
| Plan | Price | PDFs / month | Requests / min | Over the limit |
|---|---|---|---|---|
| Free | $0 | 200 | 10 | Stops with 402 |
| Starter | $19 | 5,000 | 60 | $0.005 / PDF (opt-in) |
| Growth | $49 | 20,000 | 120 | $0.004 / PDF (opt-in) |
| Scale | $99 | 60,000 | 300 | $0.003 / PDF (opt-in) |
Every feature is on every plan, including RTL, custom templates and all integrations.
Errors are JSON: {"error":{"code","message","details","requestId"}}. Branch on code; messages may change.
| Status | Code | Meaning |
|---|---|---|
Loading from /v1/errors… | ||
These are step-by-step guides and importable files built on each platform’s generic HTTP module. They are not official listed apps on those platforms yet.
Import the ready workflow: Webhook → HTTP Request (binary PDF) → respond or email.
integrations/n8n/render-invoice.workflow.jsonHTTP “Make a request” module with your key in a header; the PDF arrives as a file for the next module.
integrations/make/README.mdWebhooks by Zapier (Custom Request) with Accept: application/json, or the included Zapier CLI app.
integrations/zapier/README.md