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.

StatusError codeMeaning
401missing_api_keyHeader not sent
401invalid_api_keyKey invalid or revoked
403plan_forbiddenSubscription 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

NameTypeRequiredNotes
vatIdstringyesExact 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"
}
FieldRequiredNotes
nameyes
vatIdyesUnique per account
addressyes
countryyesISO 3166-1 alpha-2 (e.g. AT)
othersnoEmpty 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.pdf
POST /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
    }
  ]
}
FieldRequiredNotes
customerIdyesMust belong to you
invoiceTemplateIdyesMust belong to you
issueDate / dueDatenoDefaults: today / +14 days
customItemsyesAt least one
customItems[].unityesPiece, 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 codeMeaning
stripe_key_invalidRestricted API key missing/invalid/revoked
stripe_unavailableStripe 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
    }
  ]
}
FieldRequiredNotes
customerIdyesMust belong to you
invoiceTemplateIdyesMust belong to you
issueDate / dueDatenoDefaults: today / +14 days
itemsyesAt least one
items[].unityesPiece, 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.pdf
POST /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"
}
An unhandled error has occurred. Reload 🗙
Timelane

Reconnecting...

Connection lost

Connection refused

Hang tight — we'll be right back.

We couldn't re-establish the connection.

The server refused the connection.