API Reference
REST endpoints for customers, invoice templates, invoices, and self-billing. JSON in, JSON out. Sign up for a plan with API access to get a key.
Quick Start
All endpoints live under https://timelane.cloud/api. Every request must carry your API key in the X-Api-Key header.
curl https://timelane.cloud/api/customers?vatId=ATU12345678 \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5"
Bodies are JSON. Successful responses return 200/201. Errors return JSON: { "error": "code", "message": "human-readable detail" }.
Try it in Bruno. Download the ready-to-run
Timelane Bruno collection (.zip) —
open it in Bruno, set
apiKey in the prod environment, run the requests in order. Every endpoint on this page is included.
Need a key? Sign up for a Timelane account and pick a plan that includes API access — then create a restricted key under Settings → API Keys.
Authentication
Send the key in X-Api-Key. The key prefix is shown in the API Keys page; the secret part is shown once at creation. Lose it → revoke and create a new one.
| Status | Error code | Meaning |
|---|---|---|
| 401 | missing_api_key | Header not sent |
| 401 | invalid_api_key | Key invalid or revoked |
| 403 | plan_forbidden | Subscription doesn't include API access |
Every API request is recorded in your audit log regardless of outcome.
Idempotency
All POST endpoints accept an Idempotency-Key header. Send a unique key per logical request
(UUID v4 recommended). If the same key arrives again within 24h with the same body, you get the original cached
response — no duplicate row, no duplicate Stripe link, no duplicate invoice number. Same key with a
different body returns 409 idempotency_key_request_mismatch.
curl -X POST https://timelane.cloud/api/invoices \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
-H "Idempotency-Key: 0c8b3c7e-b3a7-4d2b-9e7b-9a4a3f5d2e1a" \
-H "Content-Type: application/json" \
-d '{"customerId":42,"invoiceTemplateId":1,"customItems":[{"itemKey":"DEV-01","description":"Backend development","quantity":8,"unit":"Hour","pricePerUnit":95}]}'
Use it. Network retries on POST endpoints can otherwise create duplicates.
Rate limits & auditing
No hard rate limit today, but every request is recorded with method, path, status, duration and IP. Abusive patterns may be throttled per key. Once signed in you can browse your full audit log.
Customers
GET
/api/customers?vatId={vatId}
Look up a customer by VAT ID
Returns the matching customer. Lookup is scoped to your account.
Query parameters
| Name | Type | Required | Notes |
|---|---|---|---|
vatId | string | yes | Exact match |
Response 200
{
"id": 42,
"name": "Acme GmbH",
"email": "billing@acme.example",
"phone": "+43 1 234 5678",
"address": "Musterstraße 1, 1010 Wien",
"vatId": "ATU12345678",
"contactPerson": "Anna Berger",
"buyerReference": "PO-2026-1042",
"country": "AT",
"bankName": "Erste Bank",
"iban": "AT611904300234573201",
"bic": "GIBAATWWXXX"
}
404 not_found if no customer matches.
GET
/api/customers?name={name}
Look up a customer by name (for B2C without VAT ID)
Exact, case-insensitive match on the customer name, scoped to your account. Provide
exactly one of vatId or name — both or neither returns
400 missing_parameter.
Response 200 — single match
{
"id": 47,
"name": "Max Mustermann",
"email": "max.mustermann@example.com",
"phone": "+43 660 555 1234",
"address": "Hauptstraße 12, 1010 Wien",
"vatId": null,
"contactPerson": "Max Mustermann",
"buyerReference": null,
"country": "AT",
"bankName": "Bank Austria",
"iban": "AT021200000123456789",
"bic": "BKAUATWWXXX"
}
Response 404 — no match
{
"error": "not_found",
"message": "No customer found with name 'Erika Musterfrau'"
}
Response 409 — multiple matches
Several customers share the name (common for B2C with the same person name across different addresses). The body lists them so you can pick by id:
{
"error": "ambiguous_name",
"message": "3 customers found with name 'Max Mustermann' — use the id to fetch a specific one",
"matches": [
{ "id": 47, "name": "Max Mustermann", "vatId": null },
{ "id": 88, "name": "Max Mustermann", "vatId": null },
{ "id": 124, "name": "Max Mustermann", "vatId": "DE812345678" }
]
}POST
/api/customers
Create a new customer
Supports Idempotency-Key.
Request body
{
"name": "Acme GmbH",
"vatId": "ATU12345678",
"address": "Musterstraße 1, 1010 Wien",
"country": "AT",
"email": "billing@acme.example",
"phone": "+43 1 234 5678",
"contactPerson": "Anna Berger",
"buyerReference": "PO-2026-1042",
"bankName": "Erste Bank",
"iban": "AT611904300234573201",
"bic": "GIBAATWWXXX"
}
| Field | Required | Notes |
|---|---|---|
name | yes | |
vatId | yes | Unique per account |
address | yes | |
country | yes | ISO 3166-1 alpha-2 (e.g. AT) |
| others | no | Empty string → null |
Response 201
{
"id": 42,
"name": "Acme GmbH",
"email": "billing@acme.example",
"phone": "+43 1 234 5678",
"address": "Musterstraße 1, 1010 Wien",
"vatId": "ATU12345678",
"contactPerson": "Anna Berger",
"buyerReference": "PO-2026-1042",
"country": "AT",
"bankName": "Erste Bank",
"iban": "AT611904300234573201",
"bic": "GIBAATWWXXX"
}
Response 409 — VAT ID already exists
{
"error": "vatid_exists",
"message": "Customer with vatId 'ATU12345678' already exists",
"existingId": 42,
"name": "Acme GmbH"
}PATCH
/api/customers/{id}
Update a customer (only sent fields)
Only fields present in the body are applied. Fields you don't send remain unchanged.
Request body
{
"email": "accounting@acme.example",
"phone": "+43 1 234 9999",
"iban": "AT021100000123456789"
}
Same shape as Create. country must be ISO alpha-2 if sent. vatId change returns 409 if it would clash with another customer.
Response 200
{
"id": 42,
"name": "Acme GmbH",
"email": "accounting@acme.example",
"phone": "+43 1 234 9999",
"address": "Musterstraße 1, 1010 Wien",
"vatId": "ATU12345678",
"contactPerson": "Anna Berger",
"buyerReference": "PO-2026-1042",
"country": "AT",
"bankName": "Erste Bank",
"iban": "AT021100000123456789",
"bic": "GIBAATWWXXX"
}Invoice Templates
GET
/api/invoice-templates
List your invoice templates
Returns all templates of the authenticated user, default template first.
Response 200
[
{
"id": 1,
"name": "Standard AT",
"language": "de",
"taxRate": 20.0,
"isTaxIncluded": false,
"applyTax": true,
"taxLabel": "USt",
"isDefault": true
},
{
"id": 2,
"name": "EU Reverse-Charge",
"language": "en",
"taxRate": 0.0,
"isTaxIncluded": false,
"applyTax": false,
"taxLabel": "VAT",
"isDefault": false
}
]
Use the id as invoiceTemplateId when creating an invoice or Gutschrift.
Invoices
GET
/api/invoices?customerId=&isPaid=&page=1&limit=50
List your invoices (paginated)
Newest first. All query parameters optional. Default page size 50, max 200.
Response 200
{
"items": [
{
"id": 8,
"invoiceNumber": "INV-2026-05-0008",
"customerId": 47,
"issueDate": "2026-05-18T00:00:00Z",
"dueDate": "2026-06-01T00:00:00Z",
"totalWithTax": 360.00,
"isPaid": false,
"pdfUrl": "/api/invoices/8/pdf"
},
{
"id": 7,
"invoiceNumber": "INV-2026-05-0007",
"customerId": 42,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"totalWithTax": 912.00,
"isPaid": true,
"pdfUrl": "/api/invoices/7/pdf"
},
{
"id": 6,
"invoiceNumber": "INV-2026-05-0006",
"customerId": 42,
"issueDate": "2026-05-03T00:00:00Z",
"dueDate": "2026-05-17T00:00:00Z",
"totalWithTax": 1428.00,
"isPaid": true,
"pdfUrl": "/api/invoices/6/pdf"
}
],
"page": 1,
"limit": 50,
"total": 3
}GET
/api/invoices/{id}
Fetch one invoice with totals
Returns the full Invoice DTO including the Stripe Payment Link URL (if the template uses Stripe).
{
"id": 7,
"invoiceNumber": "INV-2026-05-0007",
"customerId": 42,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"totalAmount": 760.00,
"totalWithTax": 912.00,
"taxRate": 20.0,
"isPaid": false,
"paidDate": null,
"isFinalized": true,
"finalizedAt": "2026-05-16T08:12:43Z",
"pdfUrl": "/api/invoices/7/pdf",
"stripePaymentLinkUrl": "https://buy.stripe.com/test_28o5nM4hL9bP1eMaEE"
}
404 not_found if it does not belong to you.
GET
/api/invoices/{id}/pdf
Download the archived PDF
Returns application/pdf. Hash-verified on every read.
curl https://timelane.cloud/api/invoices/7/pdf \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
-o INV-2026-05-0007.pdfPOST
/api/invoices
Create & finalize an invoice — all-or-nothing
One call creates the invoice, pulls the next number, generates and archives the hash-sealed PDF.
If the chosen template's Payment QR Code is Stripe Payment Link, we additionally
create a Stripe Payment Link on your account before consuming an invoice number, so a Stripe
failure leaves no half-built row in the DB.
Supports Idempotency-Key.
Request body
{
"customerId": 42,
"invoiceTemplateId": 1,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"notes": "Thank you for your business — payment within 14 days.",
"introductionText": "Backend development sprint, May 2026.",
"customItems": [
{
"itemKey": "DEV-01",
"description": "Backend development — API hardening",
"quantity": 8.0,
"unit": "Hour",
"pricePerUnit": 95.00,
"sortOrder": 1
},
{
"itemKey": "OPS-02",
"description": "Deployment & monitoring setup",
"quantity": 2.0,
"unit": "Hour",
"pricePerUnit": 110.00,
"sortOrder": 2
}
]
}
| Field | Required | Notes |
|---|---|---|
customerId | yes | Must belong to you |
invoiceTemplateId | yes | Must belong to you |
issueDate / dueDate | no | Defaults: today / +14 days |
customItems | yes | At least one |
customItems[].unit | yes | Piece, Hour, Day, … |
Response 201
{
"id": 9,
"invoiceNumber": "INV-2026-05-0009",
"customerId": 42,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"totalAmount": 980.00,
"totalWithTax": 1176.00,
"taxRate": 20.0,
"isPaid": false,
"paidDate": null,
"isFinalized": true,
"finalizedAt": "2026-05-18T16:42:17Z",
"pdfUrl": "/api/invoices/9/pdf",
"stripePaymentLinkUrl": "https://buy.stripe.com/test_28o5nM4hL9bP1eMaEE"
}
Response 422 — Stripe failure
{
"error": "stripe_key_invalid",
"message": "Stripe rejected the configured API key (HTTP 401): Invalid API Key provided: rk_live_***"
}
| Error code | Meaning |
|---|---|
stripe_key_invalid | Restricted API key missing/invalid/revoked |
stripe_unavailable | Stripe returned 5xx / timed out after one retry |
On 422 nothing is created in your account — the invoice number is not consumed, you can retry.
POST
/api/invoices/{id}/mark-paid
Mark an invoice as paid
Request body (optional)
{ "paidDate": "2026-05-20T00:00:00Z" }
If paidDate is omitted or null, today is used.
Response 200
{
"id": 9,
"invoiceNumber": "INV-2026-05-0009",
"customerId": 42,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"totalAmount": 980.00,
"totalWithTax": 1176.00,
"taxRate": 20.0,
"isPaid": true,
"paidDate": "2026-05-20T00:00:00Z",
"isFinalized": true,
"finalizedAt": "2026-05-18T16:42:17Z",
"pdfUrl": "/api/invoices/9/pdf",
"stripePaymentLinkUrl": "https://buy.stripe.com/test_28o5nM4hL9bP1eMaEE"
}Self-Billing
POST
/api/gutschriften
Create & finalize a self-bill
Creates a Gutschrift, finalizes it immediately, archives a hash-sealed PDF and returns a pdfUrl for download. Supports Idempotency-Key.
Request body
{
"customerId": 47,
"invoiceTemplateId": 1,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"introductionText": "Abrechnung Affiliate-Provisionen Mai 2026",
"items": [
{
"itemKey": "AFF-01",
"description": "Vermittlungsprovision Q2 — 14 Abschlüsse",
"quantity": 14.0,
"unit": "Piece",
"pricePerUnit": 45.00,
"sortOrder": 1
},
{
"itemKey": "AFF-02",
"description": "Performance-Bonus Mai",
"quantity": 1.0,
"unit": "Piece",
"pricePerUnit": 150.00,
"sortOrder": 2
}
]
}
| Field | Required | Notes |
|---|---|---|
customerId | yes | Must belong to you |
invoiceTemplateId | yes | Must belong to you |
issueDate / dueDate | no | Defaults: today / +14 days |
items | yes | At least one |
items[].unit | yes | Piece, Hour, Day, … |
Response 201
{
"id": 12,
"gutschriftNumber": "GUT-2026-05-0012",
"customerId": 47,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"totalAmount": 0,
"totalWithTax": 936.00,
"taxRate": 20.0,
"isPaid": false,
"paidDate": null,
"isFinalized": true,
"finalizedAt": "2026-05-18T16:48:02Z",
"pdfUrl": "/api/gutschriften/12/pdf"
}GET
/api/gutschriften/{id}/pdf
Download the archived PDF
Returns the finalized PDF as application/pdf. Hash-verified on every read.
curl https://timelane.cloud/api/gutschriften/12/pdf \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
-o GUT-2026-05-0012.pdfPOST
/api/gutschriften/{id}/mark-paid
Mark a Gutschrift as paid
Request body (optional)
{ "paidDate": "2026-05-20T00:00:00Z" }
If paidDate is omitted or null, today is used.
Response 200
{
"id": 12,
"gutschriftNumber": "GUT-2026-05-0012",
"customerId": 47,
"issueDate": "2026-05-16T00:00:00Z",
"dueDate": "2026-05-30T00:00:00Z",
"totalAmount": 0,
"totalWithTax": 936.00,
"taxRate": 20.0,
"isPaid": true,
"paidDate": "2026-05-20T00:00:00Z",
"isFinalized": true,
"finalizedAt": "2026-05-18T16:48:02Z",
"pdfUrl": "/api/gutschriften/12/pdf"
}