API Reference

REST endpoints for customers, invoice templates, custom units, invoices, self-billing, and flat-rate projects (offers & delivery notes). 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?search=&page=1&limit=50 List your customers (paginated)

With neither vatId nor name set, returns all your customers ordered by name. The optional search matches (case-insensitive, substring) against name and VAT ID. Default page size 50, max 200.

Query parameters

NameTypeRequiredNotes
searchstringnoSubstring of name or VAT ID
pageintno1-based, default 1
limitintno1..200, default 50

Response 200

{
  "items": [
    { "id": 42, "name": "Acme GmbH", "email": "billing@acme.example", "vatId": "ATU12345678", "country": "AT", "address": "Musterstraße 1, 1010 Wien", "phone": "+43 1 234 5678", "contactPerson": "Anna Berger", "buyerReference": "PO-2026-1042", "bankName": "Erste Bank", "iban": "AT611904300234573201", "bic": "GIBAATWWXXX" },
    { "id": 47, "name": "Max Mustermann", "email": "max.mustermann@example.com", "vatId": null, "country": "AT", "address": "Hauptstraße 12, 1010 Wien", "phone": "+43 660 555 1234", "contactPerson": "Max Mustermann", "buyerReference": null, "bankName": "Bank Austria", "iban": "AT021200000123456789", "bic": "BKAUATWWXXX" }
  ],
  "page": 1,
  "limit": 50,
  "total": 2
}
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 at most one of vatId or name — sending both returns 400 invalid_argument; sending neither returns the paginated list (see below).

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" }
  ]
}
GET /api/customers/{id} Fetch one customer by id

Returns the customer with that id (same shape as the VAT-ID lookup). Use this to resolve an entry from an ambiguous_name response.

404 not_found if it does not belong to you.

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
nameyesThe only mandatory field
vatIdnoUnique per account when set; omit for B2C / customers without a VAT ID
addressno
countrynoISO 3166-1 alpha-2 (e.g. AT) when set
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.

GET /api/invoice-templates/{id} Fetch the full configuration of one template

Returns every configurable field (colours, font, tax, payment QR, e-invoice profile, e-mail text). The logo is not inlined — hasLogo tells you whether one is set; fetch the bytes from /logo.

Response 200

{
  "id": 1,
  "name": "Standard AT",
  "language": "de",
  "primaryColor": "#1E88E5",
  "secondaryColor": "#757575",
  "fontName": "Arial",
  "isTaxIncluded": false,
  "applyTax": true,
  "taxRate": 20.0,
  "taxLabel": "USt",
  "paymentQrMode": "EpcQrCode",
  "paymentInstructions": "Please pay within 14 days to the account below.",
  "eInvoiceProfile": "EN16931",
  "businessProcessId": null,
  "mailTitle": "Invoice %InvoiceNumber% from %BusinessName%",
  "mailContent": "Dear %CustomerName%, …",
  "isDefault": true,
  "hasLogo": true
}

404 not_found if it does not belong to you.

GET /api/invoice-templates/{id}/logo Download the template logo

Returns the raw logo image (image/png or image/jpeg).

404 no_logo if the template has no logo; 404 not_found if it is not yours.

POST /api/invoice-templates Create a template

Only name is required; every other field falls back to a sensible default. The first template you create is automatically your default. Supports Idempotency-Key.

Request body

FieldTypeRequiredNotes
namestringyesMax 100 chars
languagestringno1..5 chars, e.g. "de", "en" (default "en")
primaryColor / secondaryColorstringnoHex colour, max 50 chars
fontNamestringnoMax 100 chars (default "Arial")
applyTaxboolnoWhether tax is shown at all (default true)
isTaxIncludedboolnoGross (true) vs net (false) prices
taxRatedecimalno0..100 percent (default 20)
taxLabelstringnoe.g. "USt", "VAT"; max 50 chars
paymentQrModeenumnoNone, EpcQrCode, StripePaymentLink. EpcQrCode requires the EPC plan feature
paymentInstructionsstringnoMax 500 chars
eInvoiceProfileenumnoNone, Basic, EN16931, XRechnung. Anything but None requires the e-invoice plan feature
businessProcessIdstringnoBT-23 ProfileID URN; max 200 chars
mailTitle / mailContentstringnoE-mail template with %Placeholders%; max 200 / 2000 chars
isDefaultboolnoPromote to default (demotes the previous one)
logoBase64stringnoBase64-encoded PNG or JPEG, max 2 MB

Example request

{
  "name": "Standard AT",
  "language": "de",
  "taxRate": 20.0,
  "taxLabel": "USt",
  "paymentQrMode": "EpcQrCode",
  "eInvoiceProfile": "EN16931",
  "isDefault": true
}

Responses

  • 201 CreatedInvoiceTemplateDetailDto (see GET /{id})
  • 400 invalid_argument → a field is missing or out of range
  • 403 plan_forbidden → template limit reached, or a feature (EPC QR / e-invoice) your plan does not include
PATCH /api/invoice-templates/{id} Update a template (sparse)

Only the fields you send are changed. The nullable fields taxLabel, businessProcessId and logoBase64 accept "" to clear them. Send "isDefault": true to promote this template to default; you cannot set false on the current default (promote another instead — there is always exactly one).

Example request

{
  "taxRate": 19.0,
  "taxLabel": "MwSt",
  "logoBase64": ""
}

Responses

  • 200 OK → updated InvoiceTemplateDetailDto
  • 400 invalid_argument / 400 invalid_state (e.g. un-setting the default)
  • 403 plan_forbidden → enabling a feature your plan does not include
  • 404 not_found → not yours
DELETE /api/invoice-templates/{id} Delete a template

Deleting the default template promotes another of yours to default automatically.

Responses

  • 204 No Content → deleted
  • 400 invalid_state → you cannot delete your last remaining template
  • 404 not_found → not yours

Items

GET /api/items?search=&page=1&limit=50 List / search your reusable items (paginated)

Your catalog of reusable line items, sorted by itemKey. The optional search matches (case-insensitive, substring) against itemKey and description. Default page size 50, max 200.

Query parameters

NameTypeRequiredNotes
searchstringnoSubstring of key or description
pageintno1-based, default 1
limitintno1..200, default 50

Response 200

{
  "items": [
    {
      "id": 5,
      "itemKey": "DEV-01",
      "description": "Backend development — senior rate",
      "defaultPrice": 95.00,
      "unit": "Hour",
      "customUnitId": null,
      "createdAt": "2026-05-02T09:14:00Z",
      "updatedAt": null
    },
    {
      "id": 8,
      "itemKey": "DESIGN-01",
      "description": "UI/UX design",
      "defaultPrice": 85.00,
      "unit": "Hour",
      "customUnitId": null,
      "createdAt": "2026-05-04T11:20:00Z",
      "updatedAt": "2026-05-09T07:35:00Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 2
}
GET /api/items/{id} Fetch one item
{
  "id": 5,
  "itemKey": "DEV-01",
  "description": "Backend development — senior rate",
  "defaultPrice": 95.00,
  "unit": "Hour",
  "customUnitId": null,
  "createdAt": "2026-05-02T09:14:00Z",
  "updatedAt": null
}

404 not_found if it does not belong to you.

POST /api/items Create a reusable item

The itemKey is unique per account (max 16 chars). Supports Idempotency-Key.

Request body

{
  "itemKey": "DEV-01",
  "description": "Backend development — senior rate",
  "defaultPrice": 95.00,
  "unit": "Hour",
  "customUnitId": null
}
FieldRequiredNotes
itemKeyyesUnique per account, max 16 chars
descriptionyesMax 200 chars
defaultPricenoDecimal; omit for no default
unitnoPiece, Hour, Kilogram, Liter, Meter, … (enum name) — defaults to Piece
customUnitIdnoId of one of your custom units; overrides unit

Valid unit values (case-sensitive enum names, sent/returned as strings): Piece, Kilogram, Gram, Liter, Milliliter, Hour, Minute, Meter, Centimeter, SquareMeter, Package.

Response 201

{
  "id": 5,
  "itemKey": "DEV-01",
  "description": "Backend development — senior rate",
  "defaultPrice": 95.00,
  "unit": "Hour",
  "customUnitId": null,
  "createdAt": "2026-05-18T16:42:17Z",
  "updatedAt": null
}

Response 409 — item key already exists

{
  "error": "itemkey_exists",
  "message": "Item with itemKey 'DEV-01' already exists",
  "existingId": 5,
  "itemKey": "DEV-01"
}
PATCH /api/items/{id} Update an item (only sent fields)

Only fields present in the body are applied. defaultPrice and customUnitId are updated when sent with a value — a null means "leave unchanged", not "clear". Changing itemKey to one already in use returns 409.

Request body

{
  "description": "Backend development — lead rate",
  "defaultPrice": 110.00
}

Response 200

{
  "id": 5,
  "itemKey": "DEV-01",
  "description": "Backend development — lead rate",
  "defaultPrice": 110.00,
  "unit": "Hour",
  "customUnitId": null,
  "createdAt": "2026-05-02T09:14:00Z",
  "updatedAt": "2026-05-18T16:50:03Z"
}

Custom Units

GET /api/units List your custom units

Custom units extend the built-in unit list (Hour, Piece, …). Reference one from an item or line item via customUnitId. Each carries a free label plus a standardized eInvoiceCode (UN/ECE Rec 20) so XRechnung/ZUGFeRD output stays valid.

Response 200

[
  { "id": 3, "label": "Sprint", "eInvoiceCode": "DAY", "createdAt": "2026-05-02T09:14:00Z" },
  { "id": 5, "label": "Workshop", "eInvoiceCode": "HUR", "createdAt": "2026-05-04T11:20:00Z" }
]
GET /api/units/codes List the allowed e-invoice unit codes

The set of values accepted as eInvoiceCode. Pick the one whose meaning matches your unit.

Response 200

[
  { "code": "LS",  "label": "Pauschal" },
  { "code": "H87", "label": "Stück" },
  { "code": "HUR", "label": "Stunde" },
  { "code": "DAY", "label": "Tag" },
  { "code": "KGM", "label": "Kilogramm" },
  { "code": "GRM", "label": "Gramm" },
  { "code": "MTR", "label": "Meter" },
  { "code": "MTK", "label": "Quadratmeter" },
  { "code": "LTR", "label": "Liter" },
  { "code": "MLT", "label": "Milliliter" }
]
GET /api/units/{id} Fetch one custom unit

404 not_found if it does not belong to you.

POST /api/units Create a custom unit

Only label (max 30 chars) is required. eInvoiceCode defaults to "LS" (lump sum) and must be one of GET /api/units/codes. Supports Idempotency-Key.

Example request

{ "label": "Sprint", "eInvoiceCode": "DAY" }

Responses

  • 201 CreatedUnitDto
  • 400 invalid_argument → label missing/too long, or unknown eInvoiceCode
PATCH /api/units/{id} Update a custom unit (sparse)

Only the fields you send are changed.

Responses

  • 200 OK → updated UnitDto
  • 400 invalid_argument → invalid label or code
  • 404 not_found → not yours
DELETE /api/units/{id} Delete a custom unit

Responses

  • 204 No Content → deleted
  • 409 unit_in_use → still referenced by an item/invoice/Gutschrift/project; cannot delete
  • 404 not_found → not yours

Invoices

GET /api/invoices?customerId=&isPaid=&invoiceNumber=&issuedFrom=&issuedTo=&page=1&limit=50 List your invoices (paginated)

Newest first. All query parameters optional. Default page size 50, max 200.

Query parameters

NameTypeRequiredNotes
customerIdintnoOnly invoices for this customer
isPaidboolnoFilter by payment status
invoiceNumberstringnoExact invoice-number match
issuedFromdate-timenoIssue date >= this (inclusive), e.g. 2026-01-01
issuedTodate-timenoIssue date <= this (inclusive)
pageintno1-based, default 1
limitintno1..200, default 50

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,
      "isFinalized": 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,
      "isFinalized": 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,
      "isFinalized": 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 and its id plink_… (if the template uses Stripe). The link carries invoice_id in both its metadata and payment_intent_data[metadata], so the resulting PaymentIntent can be reconciled back to this invoice.

{
  "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",
  "stripePaymentLinkId": "plink_1QZ8x2H9kPq3rLmN4tVbWcXe",
  "archivedPdfHash": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
}

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, Kilogram, Liter, Meter, … (enum name, case-sensitive)

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",
  "stripePaymentLinkId": "plink_1QZ8x2H9kPq3rLmN4tVbWcXe",
  "archivedPdfHash": "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
}

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",
  "stripePaymentLinkId": "plink_1QZ8x2H9kPq3rLmN4tVbWcXe",
  "archivedPdfHash": "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
}

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, Kilogram, Liter, Meter, … (enum name, case-sensitive)

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",
  "archivedPdfHash": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
}
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",
  "archivedPdfHash": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
}

Pauschalprojekte (flat-rate projects)

A flat-rate project bundles line items and turns them into a finalized offer (Angebot), delivery note (Lieferschein) and invoice. All endpoints in this group require a plan with Pauschalprojekt documents — otherwise they return 403 plan_forbidden. Status is derived automatically and is one of Created, Offered, Accepted, Billed, Finished.

GET /api/paushal-projects?customerId=&page=1&limit=50 List your flat-rate projects (paginated)

Newest first. Optional customerId filters to one customer. Default page size 50, max 200.

Response 200

{
  "items": [
    { "id": 31, "customerId": 42, "name": "Website Relaunch", "status": "Offered", "totalPrice": 8400.00 },
    { "id": 28, "customerId": 47, "name": "Logo & Branding", "status": "Billed", "totalPrice": 1500.00 }
  ],
  "page": 1,
  "limit": 50,
  "total": 2
}
GET /api/paushal-projects/{id} Fetch one project with its items

Response 200

{
  "id": 31,
  "customerId": 42,
  "name": "Website Relaunch",
  "description": "Full redesign incl. CMS migration",
  "status": "Offered",
  "pricingStrategy": "ItemPrice",
  "totalPrice": 8400.00,
  "hideItemPrices": false,
  "completedAt": null,
  "offerFinalized": true,
  "deliveryNoteFinalized": false,
  "items": [
    { "id": 80, "description": "UX concept & wireframes", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 2400.00, "sortOrder": 0 },
    { "id": 81, "description": "Frontend implementation", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 4000.00, "sortOrder": 1 }
  ]
}

404 not_found if it is not yours.

POST /api/paushal-projects Create a flat-rate project

Permissive: only customerId (must be yours) and name are required. You may seed initial items in the same call. Supports Idempotency-Key.

Request body

FieldTypeRequiredNotes
customerIdintyesMust belong to you
namestringyes
descriptionstringno
totalPricedecimalnoUsed by ProjectPrice / Combined strategies
pricingStrategyenumnoProjectPrice (default), ItemPrice, Combined
hideItemPricesboolnoHide per-item prices on documents
itemsarraynoEach: description (req), quantity (def 1), unit (def Piece), customUnitId, price, sortOrder

Example request

{
  "customerId": 42,
  "name": "Website Relaunch",
  "description": "Full redesign incl. CMS migration",
  "pricingStrategy": "ItemPrice",
  "items": [
    { "description": "UX concept & wireframes", "price": 2400.00 },
    { "description": "Frontend implementation", "price": 4000.00 }
  ]
}

Responses

  • 201 CreatedPaushalProjectDto (see GET /{id})
  • 400 invalid_argument → name missing
  • 404 not_found → customer not yours
  • 403 plan_forbidden → plan lacks Pauschalprojekt documents
PATCH /api/paushal-projects/{id} Update project metadata (sparse)

Only the fields you send are changed: name, description, totalPrice, pricingStrategy, hideItemPrices.

Responses

  • 200 OK → updated PaushalProjectDto
  • 404 not_found → not yours
GET /api/paushal-projects/{id}/items List a project's items

Items sorted by sortOrder.

Response 200

[
  { "id": 80, "description": "UX concept & wireframes", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 2400.00, "sortOrder": 0 },
  { "id": 81, "description": "Frontend implementation", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 4000.00, "sortOrder": 1 }
]
POST /api/paushal-projects/{id}/items Add an item to a project

Only description is required; quantity defaults to 1, unit to Piece, and sortOrder to the end. Supports Idempotency-Key.

Example request

{ "description": "CMS migration", "quantity": 1, "unit": "Piece", "price": 2000.00 }

Responses

  • 201 CreatedPaushalProjectItemDto
  • 400 invalid_argument → description missing
  • 404 not_found → project not yours
PATCH /api/paushal-projects/{id}/items/{itemId} Update a project item (sparse)

Apply any of description, quantity, unit, customUnitId, price, sortOrder.

Responses

  • 200 OK → updated PaushalProjectItemDto
  • 404 not_found → item/project not yours
DELETE /api/paushal-projects/{id}/items/{itemId} Delete a project item

Responses

  • 204 No Content → deleted
  • 404 not_found → item/project not yours
POST /api/paushal-projects/{id}/items/reorder Reorder a project's items

Pass the item ids in the desired order; sortOrder is rewritten 0..n-1. Ids not in the project are ignored.

Request body

{ "itemIds": [81, 80, 82] }

Responses

  • 204 No Content → reordered
  • 400 invalid_body → itemIds empty
  • 404 not_found → project not yours
POST /api/paushal-projects/{id}/offer Create & finalize the offer (Angebot)

Generates the offer PDF and assigns the next offer number. Supports Idempotency-Key.

Request body

{
  "invoiceTemplateId": 1,
  "issueDate": "2026-05-18T00:00:00Z",
  "validUntil": "2026-06-18T00:00:00Z",
  "deliveryTimeInDays": 30,
  "introductionText": "Thank you for your enquiry — our offer follows.",
  "footerText": "Prices exclude VAT unless stated."
}

Response 201

{
  "id": 14,
  "projectId": 31,
  "offerNumber": "ANG-2026-05-0014",
  "issueDate": "2026-05-18T00:00:00Z",
  "validUntil": "2026-06-18T00:00:00Z",
  "deliveryTimeInDays": 30,
  "isFinalized": true,
  "finalizedAt": "2026-05-18T16:48:02Z",
  "pdfUrl": "/api/paushal-projects/31/offer/pdf",
  "archivedPdfHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}

404 not_found (project/template), 400 invalid_argument / invalid_state on bad input or a project that cannot be offered.

GET /api/paushal-projects/{id}/offer/pdf Download the finalized offer PDF

Returns the archived offer PDF (hash-verified on read).

404 not_found if no offer exists for the project; 400 not_finalized if it is not finalized yet.

POST /api/paushal-projects/{id}/delivery-note Create & finalize the delivery note (Lieferschein)

Supports Idempotency-Key. Set showPrices to include prices on the note.

Request body

{
  "invoiceTemplateId": 1,
  "issueDate": "2026-05-20T00:00:00Z",
  "deliveryDate": "2026-05-22T00:00:00Z",
  "showPrices": false,
  "introductionText": "Delivery of the agreed services.",
  "footerText": null
}

Response 201

{
  "id": 9,
  "projectId": 31,
  "deliveryNoteNumber": "LS-2026-05-0009",
  "issueDate": "2026-05-20T00:00:00Z",
  "deliveryDate": "2026-05-22T00:00:00Z",
  "showPrices": false,
  "isFinalized": true,
  "finalizedAt": "2026-05-20T09:12:40Z",
  "pdfUrl": "/api/paushal-projects/31/delivery-note/pdf",
  "archivedPdfHash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
}
GET /api/paushal-projects/{id}/delivery-note/pdf Download the finalized delivery-note PDF

404 not_found if no delivery note exists; 400 not_finalized if not finalized.

POST /api/paushal-projects/{id}/invoices Create & finalize an invoice from the project

Builds the invoice line items from the project's pricing strategy (ProjectPrice → one lump-sum line, ItemPrice → one line per item, Combined → both) and finalizes it. Supports Idempotency-Key.

Request body

{
  "invoiceTemplateId": 1,
  "issueDate": "2026-05-25T00:00:00Z",
  "dueDate": "2026-06-08T00:00:00Z",
  "notes": null,
  "introductionText": "Invoice for the completed project."
}

Responses

  • 201 CreatedInvoiceDto (same shape as POST /api/invoices)
  • 400 invalid_state → project has no priced items to invoice
  • 404 not_found → project/template not yours
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.