Generate invoice PDFs and EN16931-core e-invoice XML with the InvoiceCraftly Developer Document API
Send a JSON invoice to one authenticated endpoint and get back a real, print-ready invoice PDF — rendered with the same InvoiceCraftly template your customers already see in the browser editor. Two more endpoints check EN16931-core e-invoice readiness and return the structured XML itself. No headless browser to run yourself, no HTML-to-PDF pipeline, no separate e-invoice validator to maintain.
- Endpoints
POST /api/v1/documents/pdfPOST /api/v1/invoices/readinessPOST /api/v1/documents/structured- Auth
Authorization: Bearer dk_live_…— same key for all three endpoints- Request format
- JSON invoice document,
application/json - Response format
- PDF endpoint:
application/pdf. Readiness & structured endpoints: JSON, with EN16931-core XML embedded when ready. - Free monthly quota
- 100 PDF renders per API key. Readiness & structured endpoints aren't quota-limited yet.
- Supported document
- Invoice only. PDF endpoint renders the Ledger template; XML endpoints target the EN16931-core profile, not Peppol BIS.
Prefer a typed client instead of raw HTTP?
@invoicecraftly/client is the official thin TypeScript client for the same released API documented below. It handles authentication, typed request/response shapes, PDF bytes, public API errors, timeouts and cancellation while keeping InvoiceCraftly's server authoritative for calculations, readiness and structured output.
npm install @invoicecraftly/client
Quick start
import { InvoiceCraftly } from '@invoicecraftly/client';
const client = new InvoiceCraftly({
apiKey: process.env.INVOICECRAFTLY_API_KEY
});
const pdf = await client.documents.pdf(invoice);
Keep the API key server-side. The package has no telemetry and does not implement invoice/VAT/routing/compliance rules locally.
Using Python?
invoicecraftly is the official thin Python client for the same released API. It handles bearer authentication, typed request/response shapes, PDF bytes, public API errors and timeouts while keeping InvoiceCraftly's server authoritative for invoice calculations, readiness and structured output.
pip install invoicecraftly
Quick start
import os
from invoicecraftly import InvoiceCraftly
client = InvoiceCraftly(
api_key=os.environ["INVOICECRAFTLY_API_KEY"]
)
pdf = client.documents.pdf(invoice)
Keep the API key server-side. The package has no telemetry and does not implement invoice/VAT/routing/compliance rules locally.
Prefer a complete Next.js starting point?
Clone a small App Router example that keeps INVOICECRAFTLY_API_KEY inside a server Route Handler, submits invoice fields from the browser to the local app, calls the official TypeScript client on the server, and returns the generated PDF for download.
git clone https://github.com/abaidurrehman/nextjs-invoicecraftly-example.git
cd nextjs-invoicecraftly-example
npm ci
# add INVOICECRAFTLY_API_KEY to .env.local
npm run dev
Keep INVOICECRAFTLY_API_KEY server-side and never rename it to NEXT_PUBLIC_INVOICECRAFTLY_API_KEY. This is a runnable source example, not a Next.js partnership or a claim that InvoiceCraftly hosts a live demo.
1. Get an API key
Sign in with your InvoiceCraftly account, then generate a key from the account page. Only one key is active per account — generating a new key immediately revokes the previous one. The full key is shown exactly once, at generation time, so store it somewhere safe.
2. Authenticate your request
Every request to the Developer Document API carries your key as a bearer token in the Authorization header. Keys always start with the dk_live_ prefix, which makes a leaked key easy to spot and rotate.
Authorization: Bearer dk_live_YOUR_API_KEY_HERE
Content-Type: application/json
A missing, malformed, or revoked key returns 401 with error code AUTHENTICATION_FAILED. Query-string keys are never accepted — the key must be sent in the Authorization header.
3. Send an invoice, get a PDF back
The request body is a single JSON invoice document — no wrapper object, no multi-step upload. Save this fixture as invoice.json in your project; every example below sends this exact document:
{
"type": "invoice",
"number": "INV-1042",
"issueDate": "2026-09-05",
"dueDate": "2026-09-19",
"currency": "USD",
"seller": {
"name": "Fixture Seller Studio",
"addressLines": ["12 Render Way", "Austin, TX 73301", "United States"]
},
"buyer": {
"name": "Fixture Buyer Co",
"addressLines": ["400 Client Ave", "Denver, CO 80202", "United States"]
},
"items": [
{
"description": "Brand design retainer",
"quantity": 2,
"unitPrice": 150,
"taxRate": 8.25,
"taxLabel": "Sales Tax"
}
],
"payment": {
"iban": "DE89370400440532013000",
"reference": "INV-1042",
"terms": "Net 14"
}
}
curl https://invoicecraftly.com/api/v1/documents/pdf \
-X POST \
-H "Authorization: Bearer dk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
--data @invoice.json \
--output invoice.pdf
// Node.js 18+, built-in fetch — no dependency install
import { readFileSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
const invoice = JSON.parse(readFileSync('invoice.json', 'utf8'));
const response = await fetch('https://invoicecraftly.com/api/v1/documents/pdf', {
method: 'POST',
headers: {
Authorization: 'Bearer dk_live_YOUR_API_KEY_HERE',
'Content-Type': 'application/json'
},
body: JSON.stringify(invoice)
});
if (!response.ok) throw new Error(`Render failed: ${response.status}`);
await writeFile('invoice.pdf', Buffer.from(await response.arrayBuffer()));
# pip install requests
import json
import requests
with open("invoice.json") as f:
invoice = json.load(f)
response = requests.post(
"https://invoicecraftly.com/api/v1/documents/pdf",
headers={
"Authorization": "Bearer dk_live_YOUR_API_KEY_HERE",
"Content-Type": "application/json",
},
json=invoice,
)
response.raise_for_status()
with open("invoice.pdf", "wb") as f:
f.write(response.content)
// Plain PHP, curl extension only — no Composer package required
$invoice = file_get_contents('invoice.json');
$ch = curl_init('https://invoicecraftly.com/api/v1/documents/pdf');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer dk_live_YOUR_API_KEY_HERE',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $invoice,
CURLOPT_RETURNTRANSFER => true,
]);
$pdf = curl_exec($ch);
file_put_contents('invoice.pdf', $pdf);
curl_close($ch);
A successful call returns 200 with Content-Type: application/pdf and the finished document as the response body — write it straight to a file or stream it to your own user. An X-Render-Duration-Ms header reports how long the render took.
4. Check EN16931-core e-invoice readiness
Before you commit to generating a structured e-invoice, check whether an invoice is ready — and see exactly what's missing if it isn't. This endpoint returns diagnostics only, no XML. The body is the same document object as the PDF endpoint, plus an optional supplement object carrying EN16931-core-specific fields (postal addresses, tax identifiers, per-line unit and VAT category codes) that a plain invoice document doesn't otherwise capture:
{
"document": { /* same shape as the PDF endpoint's invoice.json */ },
"supplement": {
"version": 1,
"seller": {
"postalAddress": { "countryCode": "NO", "city": "Oslo", "postalCode": "0154" },
"taxId": { "role": "legal-registration", "schemeId": "0192" },
"vatIdentifier": "NO123456785MVA"
},
"buyer": {
"postalAddress": { "countryCode": "NO", "city": "Oslo", "postalCode": "0184" }
},
"lines": [
{ "sourceIndex": 0, "unitCode": "HUR", "vatCategoryCode": "S" }
]
}
}
curl https://invoicecraftly.com/api/v1/invoices/readiness \
-X POST \
-H "Authorization: Bearer dk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
--data @readiness-request.json
// Node.js 18+, built-in fetch — no dependency install
import { readFileSync } from 'node:fs';
const requestBody = readFileSync('readiness-request.json', 'utf8');
const response = await fetch('https://invoicecraftly.com/api/v1/invoices/readiness', {
method: 'POST',
headers: {
Authorization: 'Bearer dk_live_YOUR_API_KEY_HERE',
'Content-Type': 'application/json'
},
body: requestBody
});
const readiness = await response.json();
console.log(readiness.ready, readiness.gaps);
# pip install requests
import json
import requests
with open("readiness-request.json") as f:
request_body = json.load(f)
response = requests.post(
"https://invoicecraftly.com/api/v1/invoices/readiness",
headers={
"Authorization": "Bearer dk_live_YOUR_API_KEY_HERE",
"Content-Type": "application/json",
},
json=request_body,
)
response.raise_for_status()
readiness = response.json()
print(readiness["ready"], readiness["gaps"])
// Plain PHP, curl extension only — no Composer package required
$requestBody = file_get_contents('readiness-request.json');
$ch = curl_init('https://invoicecraftly.com/api/v1/invoices/readiness');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer dk_live_YOUR_API_KEY_HERE',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $requestBody,
CURLOPT_RETURNTRANSFER => true,
]);
$readiness = json_decode(curl_exec($ch), true);
curl_close($ch);
A successful call returns 200 with a JSON body — never a 422 for an invoice that's merely incomplete, since "not ready yet" is a normal, expected outcome here:
{
"apiVersion": "v1",
"profileId": "en16931-core",
"specificationIdentifier": "urn:cen.eu:en16931:2017",
"ready": false,
"gaps": [
{ "id": "EN16931-CORE-SELLER-VAT-IDENTIFIER", "message": "Seller is missing a VAT identifier." }
]
}
There's no artifact or xml field in this response — this endpoint reports diagnostics only. Once gaps is empty and ready is true, call the structured artifact endpoint below to get the actual XML.
5. Get the EN16931-core XML artifact
Same request body as the readiness endpoint above — document plus optional supplement — but the response also carries the rendered EN16931-core UBL/XML when the invoice is ready:
curl https://invoicecraftly.com/api/v1/documents/structured \
-X POST \
-H "Authorization: Bearer dk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
--data @readiness-request.json
// Node.js 18+, built-in fetch — no dependency install
import { readFileSync, writeFileSync } from 'node:fs';
const requestBody = readFileSync('readiness-request.json', 'utf8');
const response = await fetch('https://invoicecraftly.com/api/v1/documents/structured', {
method: 'POST',
headers: {
Authorization: 'Bearer dk_live_YOUR_API_KEY_HERE',
'Content-Type': 'application/json'
},
body: requestBody
});
const result = await response.json();
if (result.ready) writeFileSync('invoice.xml', result.artifact.content);
# pip install requests
import json
import requests
with open("readiness-request.json") as f:
request_body = json.load(f)
response = requests.post(
"https://invoicecraftly.com/api/v1/documents/structured",
headers={
"Authorization": "Bearer dk_live_YOUR_API_KEY_HERE",
"Content-Type": "application/json",
},
json=request_body,
)
response.raise_for_status()
result = response.json()
if result["ready"]:
with open("invoice.xml", "w", encoding="utf-8") as f:
f.write(result["artifact"]["content"])
// Plain PHP, curl extension only — no Composer package required
$requestBody = file_get_contents('readiness-request.json');
$ch = curl_init('https://invoicecraftly.com/api/v1/documents/structured');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer dk_live_YOUR_API_KEY_HERE',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $requestBody,
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
if ($result['ready']) file_put_contents('invoice.xml', $result['artifact']['content']);
A successful call returns 200 with JSON. When ready, artifact.content holds the EN16931-core XML as a string; when not ready, artifact is null and gaps explains why:
{
"apiVersion": "v1",
"profileId": "en16931-core",
"specificationIdentifier": "urn:cen.eu:en16931:2017",
"ready": true,
"gaps": [],
"artifact": { "mediaType": "application/xml", "content": "<Invoice ...>...</Invoice>" }
}
This is EN16931-core UBL/XML, not a Peppol BIS artifact. There's no network delivery, no Access Point transmission, and no official-conformance claim — it's a structured export you can feed into your own validation or delivery pipeline, not a finished Peppol e-invoice.
Invoice document fields
The request body is a PublicDocumentV1 invoice: plain, versioned JSON — not InvoiceCraftly's internal editor format. Unknown fields are rejected outright rather than silently ignored.
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | Yes | Must be "invoice" — other document types are recognized but not yet rendered (TARGET_NOT_SUPPORTED). |
number | string | No | Your own invoice number. |
issueDate | string | Yes | ISO date, YYYY-MM-DD. |
dueDate | string | No | ISO date, YYYY-MM-DD. |
currency | string | Yes | 3-letter uppercase currency code, e.g. USD, EUR, DKK. |
locale | string | No | BCP 47 locale for number/date formatting, e.g. da-DK. |
template | string | No | Must be "ledger" if present — the only template this endpoint renders today. |
seller.name / buyer.name | string | Yes | Non-empty business or individual name. |
seller.addressLines / buyer.addressLines | string[] | Yes | Each line rendered as written. |
items[] | array | Yes | At least one line item. |
items[].description | string | Yes | Line item description. |
items[].quantity | number | Yes | Greater than 0. |
items[].unitPrice | number | Yes | 0 or greater. |
items[].taxRate | number | Yes | 0–100. |
items[].taxLabel | string | No | e.g. "VAT", "Sales Tax", "Moms". |
payment | object | No | bankName, iban, bic, accountNumber, routingNumber, reference, poNumber, terms, dueText, qrMode. |
notes | string | No | Free-text note printed on the document. |
branding | object | No | accentColor (hex), stampStyle (round/square/none), logo (data base64 + mimeType, PNG/JPEG, 512 KB decoded max). |
The readiness and structured artifact endpoints wrap this same document in a request envelope, plus an optional supplement object carrying EN16931-core-specific fields a plain invoice document doesn't capture:
| Field | Type | Required | Notes |
|---|---|---|---|
version | number | No | Must be 1 if present. |
buyerReference | string | No | Buyer's own reference/order code. |
seller.postalAddress / buyer.postalAddress | object | No | countryCode (ISO 3166-1 alpha-2), city, postalCode, region. |
seller.taxId | object | No | role (seller-id / legal-registration / tax-registration / vat), schemeId, taxSchemeId. |
seller.vatIdentifier / buyer.vatIdentifier | string | No | VAT identifier, e.g. NO123456785MVA. |
lines[].sourceIndex | number | Yes, within each line entry | 0-based index matching a position in the document.items array. |
lines[].unitCode | string | No | UN/CEFACT unit code, e.g. HUR, C62. |
lines[].vatCategoryCode | string | No | One of AE, B, E, G, K, L, M, O, S, Z. |
Missing supplement fields aren't rejected — they simply show up as entries in the response's gaps array, so you can call the readiness endpoint first, fill in what it flags, then re-check.
Errors
Every error — validation, authentication, quota, or render failure — comes back as the same JSON envelope, never a raw stack trace or an undocumented code:
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please slow down.",
"requestId": null,
"details": []
}
}
| HTTP status | Code | Meaning |
|---|---|---|
| 400 / 413 / 415 | INVALID_REQUEST | Malformed JSON, unknown field, oversized body, or wrong content type. |
| 401 | AUTHENTICATION_FAILED | Missing, malformed, or revoked API key. |
| 422 | TARGET_NOT_SUPPORTED | Structurally valid document, but the requested type or template isn't rendered yet. |
| 429 | RATE_LIMITED | Monthly quota reached on the PDF endpoint; a Retry-After header reports seconds until reset. The readiness and structured endpoints don't quota-limit yet. |
| 503 / 504 | RENDER_TIMEOUT / PDF_RENDER_FAILED | PDF endpoint only — the render didn't finish in time, or failed on InvoiceCraftly's side. Safe to retry. |
API changelog
This tracks changes to the /v1 contract itself — request/response shape, authentication, and quota behaviour. For product-wide feature announcements across the whole InvoiceCraftly app, see the changelog.
- 2026-09-06 — Added
POST /api/v1/invoices/readiness(EN16931-core e-invoice readiness diagnostics, no artifact) andPOST /api/v1/documents/structured(readiness diagnostics plus the EN16931-core UBL/XML artifact when ready). Both reuse the same API key as the PDF endpoint, aren't quota-limited yet, and never claim Peppol BIS conformance. - 2026-09-05 —
v1beta launched:POST /api/v1/documents/pdf, invoice document type with the Ledger template only, API keys with a 100-render monthly quota.
Current beta scope
All three endpoints handle one document type (invoice) only, and a request body capped around 900 KB (roughly a 512 KB logo once decoded, plus the rest of the invoice). The PDF endpoint renders one layout (the Ledger template) and is billed against your account's 100-render monthly quota regardless of success or failure once the render actually starts; the readiness and structured artifact endpoints aren't quota-limited yet. All three process your invoice payload on InvoiceCraftly's server — that's different from the browser editor, which keeps documents on your own device. Read the engineering notes on that data boundary and the privacy policy for the current retention behaviour. The structured XML endpoint targets the EN16931-core profile only — no Peppol BIS artifact, no Access Point delivery, and no official-conformance claim. There's no quote/estimate/receipt/credit-note rendering yet, and no billing beyond the free monthly quota.
Frequently asked questions
Does the API support anything besides invoices?
Not yet — every endpoint works with one document type (invoice) only, and the PDF endpoint renders the Ledger template only. See the current limits for what's not supported.
Can I use my own invoice template with the API?
Not yet. The PDF endpoint renders the same Ledger template available in the browser editor — there's no per-request template choice today.
Is there a free tier?
Yes — 100 PDF renders per month per API key during the beta, with no billing yet. The readiness and structured XML endpoints share the same key but aren't quota-limited yet.
Do I need a backend to use this, or can I call it from the browser?
A backend. Your API key is a secret and must never be embedded in client-side JavaScript that ships to a browser.
What happens if I exceed my quota?
You get back 429 with error code RATE_LIMITED and a Retry-After header. See the error table for the full list of codes.
Does the structured XML endpoint produce Peppol-conformant e-invoices?
No. It produces EN16931-core UBL/XML only — no Peppol BIS artifact, network delivery, or official-conformance claim. Treat it as a starting point, not a final compliance decision.
What's the difference between the readiness endpoint and the structured artifact endpoint?
POST /api/v1/invoices/readiness returns diagnostics only — whether the invoice is ready and what's missing. POST /api/v1/documents/structured returns the same diagnostics plus the EN16931-core XML itself once the invoice is ready.
Get an API key and generate your first invoice PDF.
Free during the beta, 100 renders included per month.