API-Referenz
REST-Endpoints für Kunden, Vorlagen, eigene Einheiten, Rechnungen, Gutschriften und Pauschalprojekte (Angebote & Lieferscheine). JSON rein, JSON raus. Registrieren Sie sich für einen Tarif mit API-Zugang, um einen Schlüssel zu erhalten. Die Endpoint-Dokumentation selbst ist englisch — sie steht neben englischen Feldnamen und Fehlercodes.
Schnellstart
Alle Endpoints liegen unter https://timelane.cloud/api. Jede Anfrage muss Ihren API-Schlüssel im Header X-Api-Key mitschicken.
curl https://timelane.cloud/api/customers?vatId=ATU12345678 \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5"
Anfrage- und Antwortkörper sind JSON. Erfolgreiche Antworten liefern 200/201. Fehler liefern JSON: { "error": "code", "message": "human-readable detail" }.
In Bruno ausprobieren. Laden Sie die fertige
Timelane-Bruno-Collection (.zip) herunterladen —
in Bruno öffnen,
apiKey in der Umgebung prod setzen und die Requests der Reihe nach ausführen. Jeder Endpoint dieser Seite ist enthalten.
Noch kein Schlüssel? Registrieren Sie sich für ein Timelane-Konto und wählen Sie einen Tarif mit API-Zugang — danach legen Sie unter Konfiguration → API-Schlüssel einen eingeschränkten Schlüssel an.
Authentifizierung
Schicken Sie den Schlüssel im Header X-Api-Key. Das Präfix ist auf der Seite API-Schlüssel sichtbar, der geheime Teil wird einmalig beim Anlegen angezeigt. Verloren? Widerrufen und einen neuen anlegen.
| Status | Fehlercode | Bedeutung |
|---|---|---|
| 401 | missing_api_key | Header nicht mitgeschickt |
| 401 | invalid_api_key | Schlüssel ungültig oder widerrufen |
| 403 | plan_forbidden | Tarif enthält keinen API-Zugang |
Jede API-Anfrage wird unabhängig vom Ergebnis in Ihrem Protokoll festgehalten.
Idempotenz
Alle POST-Endpoints akzeptieren den Header Idempotency-Key. Schicken Sie pro logischer Anfrage
einen eindeutigen Schlüssel (UUID v4 empfohlen). Trifft derselbe Schlüssel innerhalb von 24 Stunden mit demselben
Körper erneut ein, erhalten Sie die zwischengespeicherte Originalantwort — kein doppelter Datensatz, kein doppelter
Stripe-Link, keine doppelte Rechnungsnummer. Derselbe Schlüssel mit anderem Körper liefert
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}]}'
Nutzen Sie ihn. Netzwerk-Wiederholungen auf POST-Endpoints erzeugen sonst Duplikate.
Die …/preview-Endpoints sind die Ausnahme — sie speichern nichts, also gibt es nichts zu wiederholen und es wird kein Schlüssel benötigt.
Ratenbegrenzung & Protokollierung
Derzeit keine harte Ratenbegrenzung, aber jede Anfrage wird mit Methode, Pfad, Status, Dauer und IP festgehalten. Missbräuchliche Muster können pro Schlüssel gedrosselt werden. Nach der Anmeldung können Sie Ihr vollständiges Protokoll einsehen.
Customers
6 endpoints
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
| Name | Type | Required | Notes |
|---|---|---|---|
search | string | no | Substring of name or VAT ID |
page | int | no | 1-based, default 1 |
limit | int | no | 1..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
| 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
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"
}
| Field | Required | Notes |
|---|---|---|
name | yes | The only mandatory field |
vatId | no | Unique per account when set; omit for B2C / customers without a VAT ID |
address | no | |
country | no | ISO 3166-1 alpha-2 (e.g. AT) when set |
| 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
6 endpoints
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
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Max 100 chars |
language | string | no | 1..5 chars, e.g. "de", "en" (default "en") |
primaryColor / secondaryColor | string | no | Hex colour, max 50 chars |
fontName | string | no | Max 100 chars (default "Arial") |
applyTax | bool | no | Whether tax is shown at all (default true) |
isTaxIncluded | bool | no | Gross (true) vs net (false) prices |
taxMode | enum | no | NoTax, UniformNet, UniformGross, PerLineNet, PerLineGross — single-dropdown view over the two bools plus per-line tax; wins when sent alongside them. Per-line modes allow taxRate/taxCategory on document lines (mixed baskets). |
zeroTaxCategory | string | no | Z (zero rated, default), AE (reverse charge), G (export) — EN16931 semantics of 0 %-rated lines |
taxExemptionReason | string | no* | BT-120 exemption text, max 500 chars. Required when zeroTaxCategory is AE or G — enforced at template create/update, not at finalize. |
taxRate | decimal | no | 0..100 percent (default 20); the default rate for lines without an explicit one |
taxLabel | string | no | e.g. "USt", "VAT"; max 50 chars |
paymentQrMode | enum | no | None, EpcQrCode, StripePaymentLink. EpcQrCode requires the EPC plan feature |
paymentInstructions | string | no | Max 500 chars |
eInvoiceProfile | enum | no | None, Basic, EN16931, XRechnung. Anything but None requires the e-invoice plan feature |
businessProcessId | string | no | BT-23 ProfileID URN; max 200 chars |
mailTitle / mailContent | string | no | E-mail template with %Placeholders%; max 200 / 2000 chars |
isDefault | bool | no | Promote to default (demotes the previous one) |
logoBase64 | string | no | Base64-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 Created→InvoiceTemplateDetailDto(see GET /{id})400 invalid_argument→ a field is missing or out of range403 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→ updatedInvoiceTemplateDetailDto400 invalid_argument/400 invalid_state(e.g. un-setting the default)403 plan_forbidden→ enabling a feature your plan does not include404 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→ deleted400 invalid_state→ you cannot delete your last remaining template404 not_found→ not yours
Business Settings
4 endpoints
GET
/api/business-info
Read business identity, bank data and number-circle prefixes
Your own business record — identity, bank data and all document-number prefixes. Secrets (SMTP password, Stripe key) are never exposed here. Returns 404 not_found until the record exists (create it via PATCH).
Response 200
{
"name": "QSP Solutions e.U.",
"ownerName": "Bernhard Muster",
"address": "Hauptplatz 3\n8010 Graz",
"additionalHeaderLines": "FN 123456a, LG Musterstadt",
"email": "billing@qsp-solutions.at",
"phone": "+43 316 123456",
"vatId": "ATU58291736",
"country": "AT",
"bankName": "Bank Austria",
"accountHolder": "QSP Solutions e.U.",
"iban": "AT611904300234573201",
"bic": "BKAUATWW",
"invoicePrefix": "RE",
"offerPrefix": "AN",
"gutschriftPrefix": "GUT",
"deliveryNotePrefix": "LS",
"orderPrefix": "AB",
"purchaseOrderPrefix": "BE",
"creditNotePrefix": "KR"
}PATCH
/api/business-info
Update business info (partial)
Partial update: only sent fields change, "" clears a nullable field. Creating the record for the first time requires name. Bank switch, GmbH conversion, new prefixes — all self-service, no admin key needed.
Request body (example: bank switch)
{
"bankName": "Erste Bank",
"accountHolder": "QSP Solutions GmbH",
"iban": "AT483200000012345864",
"bic": "GIBAATWWXXX"
}
accountHolder is the name printed after the "Account Holder" label on documents. Prefix fields accept up to 10 characters; they apply to newly issued numbers only.
address must contain the postal address only — it is parsed into the street, post code and city fields of every e-invoice. Commercial register numbers, courts or supervisory authorities belong in additionalHeaderLines (max 500 characters), which is printed below the address on documents and never enters the e-invoice XML.
GET
/api/number-sequences
List all number circles
Effective prefix, number template and current counter for each of the seven document-number circles. highestSequence is the counter of the circle's current period: a circle with resetScope yearly reports the count within this year, not since the beginning.
Templates are managed in the app (Nummernkreise) and are read-only here. Placeholders: {PREFIX}, {yyyy}, {yy}, {MM}, {dd}, {NR} / {NR:0000}. Circles without a template render through the default pattern shown below.
Response 200
[
{ "documentType": "invoices", "prefix": "RE", "highestSequence": 142, "templateName": "Jährlich vierstellig", "pattern": "{PREFIX}-{yyyy}-{NR:0000}", "resetScope": "yearly" },
{ "documentType": "credit-notes", "prefix": "KR", "highestSequence": 3, "templateName": "Standard", "pattern": "{PREFIX}-{yyyy}-{MM}-{NR:0000}", "resetScope": "never" },
{ "documentType": "gutschriften", "prefix": "GUT", "highestSequence": 12, "templateName": "Standard", "pattern": "{PREFIX}-{yyyy}-{MM}-{NR:0000}", "resetScope": "never" },
{ "documentType": "offers", "prefix": "AN", "highestSequence": 57, "templateName": "Standard", "pattern": "{PREFIX}-{yyyy}-{MM}-{NR:0000}", "resetScope": "never" },
{ "documentType": "orders", "prefix": "AB", "highestSequence": 31, "templateName": "Standard", "pattern": "{PREFIX}-{yyyy}-{MM}-{NR:0000}", "resetScope": "never" },
{ "documentType": "delivery-notes", "prefix": "LS", "highestSequence": 24, "templateName": "Standard", "pattern": "{PREFIX}-{yyyy}-{MM}-{NR:0000}", "resetScope": "never" },
{ "documentType": "purchase-orders", "prefix": "BE", "highestSequence": 8, "templateName": "Standard", "pattern": "{PREFIX}-{yyyy}-{MM}-{NR:0000}", "resetScope": "never" }
]POST
/api/number-sequences/{documentType}/reset
Reset a number circle
Sets the counter of one circle — the next issued number is startAt + 1. documentType is one of the values from the list endpoint. Only the current period is affected: on a circle that resets yearly, this rewinds the current year and leaves earlier years alone. Use with care: lowering the counter can produce duplicate document numbers; sequential numbering is a legal requirement for invoices.
Request body
{ "startAt": 100 }
Response 200
{ "documentType": "invoices", "prefix": "RE", "highestSequence": 100, "templateName": "Jährlich vierstellig", "pattern": "{PREFIX}-{yyyy}-{NR:0000}", "resetScope": "yearly" }Invoices
6 endpoints
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
| Name | Type | Required | Notes |
|---|---|---|---|
customerId | int | no | Only invoices for this customer |
isPaid | bool | no | Filter by payment status |
invoiceNumber | string | no | Exact invoice-number match |
issuedFrom | date-time | no | Issue date >= this (inclusive), e.g. 2026-01-01 |
issuedTo | date-time | no | Issue date <= this (inclusive) |
page | int | no | 1-based, default 1 |
limit | int | no | 1..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",
"taxBreakdown": [
{ "taxCategory": "S", "taxRate": 20.0, "netAmount": 760.00, "taxAmount": 152.00 }
],
"items": [
{
"itemKey": "DEV-01",
"description": "Backend development — API hardening",
"quantity": 8.0,
"unit": "Hour",
"pricePerUnit": 95.00,
"taxRate": 20.0,
"taxCategory": "S",
"creditedQuantity": 2.0,
"remainingQuantity": 6.0,
"sortOrder": 1
}
],
"creditedAmount": 190.00,
"remainingAmount": 570.00,
"isFullyCredited": false,
"creditNotes": [
{ "id": 4711, "creditNoteNumber": "KR-2026-07-0003", "totalWithTax": -228.00, "issueDate": "2026-07-15T00:00:00Z" }
]
}
404 not_found if it does not belong to you. The correction-status fields (items, creditedAmount, remainingAmount, isFullyCredited, creditNotes) are populated on this detail endpoint; use remainingQuantity per line to build valid credit-note requests.
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,
"taxRate": 13.0,
"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, Kilogram, Liter, Meter, … (enum name, case-sensitive) |
customItems[].taxRate | no | Per-line rate (0–100, max 2 decimals) for mixed baskets. Requires a template with a per-line tax mode, otherwise 400 invalid_argument. Omitted → template rate. |
customItems[].taxCategory | no | EN16931 VAT category (S, Z, AE, G, …). Omitted → derived: S for rate > 0, else the template's zeroTaxCategory. |
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": 1174.60,
"taxRate": null,
"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",
"taxBreakdown": [
{ "taxCategory": "S", "taxRate": 20.0, "netAmount": 760.00, "taxAmount": 152.00 },
{ "taxCategory": "S", "taxRate": 13.0, "netAmount": 220.00, "taxAmount": 28.60 }
]
}
taxRate is filled when all lines share one effective rate and null for mixed-rate documents — taxBreakdown (one entry per category+rate, matching the e-invoice BG-23 groups) is always present and is the reliable source. Tax is computed per rate group: net amounts summed exactly, then rounded, then taxed (EN16931 BR-CO-17).
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/preview
See the invoice before you issue it — nothing is persisted
Dry run of POST /api/invoices. Takes the identical body and returns
application/pdf showing exactly what the real call would produce. Nothing is persisted:
no invoice row, no invoice number consumed, no Stripe Payment Link created, nothing
written to the archive. Build and check your integration without burning numbers.
What differs from the real document
| On the preview | Why |
|---|---|
Invoice number reads INV-2026-05-PREVIEW | Your real prefix and year-month, but no sequence — the number circle is untouched |
Payment QR points at timelane.cloud | A preview must never carry a payable EPC/SEPA or Stripe payload. Position and size are unchanged, so the layout matches the real PDF. |
| No embedded e-invoice XML, no PDF/A | A preview is a visual proof, not an e-invoice — even when your template has a ZUGFeRD/XRechnung profile |
Everything else — amounts, per-line and grouped tax, template layout, logo, texts, references — is the real, computed result.
curl -X POST https://timelane.cloud/api/invoices/preview \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
-H "Content-Type: application/json" \
-d '{"customerId":42,"invoiceTemplateId":1,"customItems":[{"itemKey":"DEV-01","description":"Backend development","quantity":8,"unit":"Hour","pricePerUnit":95}]}' \
-o preview.pdf
Validation is shared with the real call, so a payload that previews successfully is a payload
POST /api/invoices accepts — same 404 not_found,
400 invalid_argument, 422 business_info_missing. One deliberate exception:
a Stripe template previews fine without a Stripe API key configured, where the real call
returns 422 stripe_key_invalid. No Idempotency-Key needed — there is nothing to replay.
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"
}Credit Notes
5 endpoints
POST
/api/invoices/{invoiceId}/credit-notes
Create & finalize a credit note (Rechnungskorrektur)
Corrects a finalized invoice — fully (omit items → Storno over all remaining quantities) or per line (Teilgutschrift). The credit note finalizes immediately, gets its own number circle (KR-… by default) and inherits template, language, tax treatment and e-invoice profile from the original invoice — a correction can never carry a different tax treatment. Amounts are stored and rendered negative; the embedded e-invoice XML uses type 381 with positive amounts and the mandatory reference to the original invoice (BG-3). Supports Idempotency-Key.
Request body (partial credit)
{
"reason": "Retoure 2 Stück beschädigt",
"issueDate": "2026-07-15T00:00:00Z",
"items": [
{
"itemKey": "ART-00098",
"quantity": 2,
"pricePerUnit": 9.99,
"description": "Retoure beschädigt"
}
]
}
| Field | Required | Notes |
|---|---|---|
reason | no | Printed as introduction text on the document |
issueDate | no | Default: today |
items | no | Omit entirely for a full storno (all remaining quantities) |
items[].itemKey | yes | Must be a line of the original invoice; duplicate keys with the same rate pool their quantities |
items[].quantity | yes | Positive; ≤ remaining quantity (original − already credited, cumulative over all credit notes) |
items[].pricePerUnit | no | Default: original line price; must not exceed it (price corrections only downwards) |
items[].description | no | Default: original line description |
items[].taxRate | no | Only needed as discriminator when the key exists with more than one rate on the invoice |
Response 201
{
"id": 4711,
"creditNoteNumber": "KR-2026-07-0003",
"invoiceId": 1234,
"invoiceNumber": "INV-2026-07-0815",
"issueDate": "2026-07-15T00:00:00Z",
"reason": "Retoure 2 Stück beschädigt",
"totalAmount": -19.98,
"totalWithTax": -22.58,
"taxRate": 13.0,
"isSettled": false,
"settledDate": null,
"isFinalized": true,
"finalizedAt": "2026-07-15T09:12:00Z",
"pdfUrl": "/api/credit-notes/4711/pdf",
"archivedPdfHash": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e",
"taxBreakdown": [
{ "taxCategory": "S", "taxRate": 13.0, "netAmount": -19.98, "taxAmount": -2.60 }
]
}
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | Invoice does not exist / not yours |
| 400 | invalid_state | Invoice is not finalized |
| 409 | over_credit | Requested quantity/amount exceeds the remaining creditable rest (message names the itemKey and rest) |
| 409 | partial_not_supported | Invoice has time-based lines or a total-amount override — only full storno (omit items) |
| 400 | ambiguous_item | itemKey exists with several tax rates — send taxRate as discriminator |
| 400 | invalid_argument | Unknown itemKey, quantity ≤ 0, pricePerUnit above the original |
Correction status on the invoice: GET /api/invoices/{id} now returns items[] (with creditedQuantity/remainingQuantity per line), creditedAmount, remainingAmount, isFullyCredited and creditNotes[]. isPaid of the invoice stays untouched — payment and correction are separate axes; isFullyCredited is the truth for "storniert".
POST
/api/invoices/{invoiceId}/credit-notes/preview
See the correction before you issue it — nothing is persisted
Dry run of the create call above. Identical body, returns application/pdf — no credit note
row, no KR number consumed, nothing archived. The number reads
KR-2026-07-PREVIEW and no e-invoice XML is embedded. Omit items to preview a
full storno.
The full pool math and every guard of the real call run here too, so the same rejections apply —
over_credit, partial_not_supported, ambiguous_item,
invalid_state. Checking a correction before it becomes permanent is worth more here than
anywhere else: a credit note cannot be taken back.
curl -X POST https://timelane.cloud/api/invoices/1234/credit-notes/preview \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
-H "Content-Type: application/json" \
-d '{"reason":"Retoure","items":[{"itemKey":"ART-00098","quantity":2}]}' \
-o preview.pdf
Not transactional: the already-credited state it validates against is a snapshot taken at request time. A concurrent real credit note can still change what remains creditable.
GET
/api/credit-notes
List credit notes
Paginated (page, limit ≤ 200), newest first. Filters: invoiceId, issuedFrom, issuedTo.
curl "https://timelane.cloud/api/credit-notes?invoiceId=1234&page=1&limit=50" \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5"
Single fetch: GET /api/credit-notes/{id} — same shape as the create response.
GET
/api/credit-notes/{id}/pdf
Download the archived PDF
Returns the finalized PDF as application/pdf. Hash-verified on every read. The document carries the title "Korrekturrechnung", the reference block "zu Rechnung {invoiceNumber} vom {issueDate}", negative amounts — and deliberately no payment block (no EPC QR, no Stripe link).
POST
/api/credit-notes/{id}/mark-settled
Mark a credit note as settled
The counterpart of mark-paid: the refund was paid out or offset against another invoice.
Request body (optional)
{ "settledDate": "2026-07-20T00:00:00Z" }
If settledDate is omitted or null, today is used. Returns the full credit-note object.
Items
4 endpoints
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
| Name | Type | Required | Notes |
|---|---|---|---|
search | string | no | Substring of key or description |
page | int | no | 1-based, default 1 |
limit | int | no | 1..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
}
| Field | Required | Notes |
|---|---|---|
itemKey | yes | Unique per account, max 16 chars |
description | yes | Max 200 chars |
defaultPrice | no | Decimal; omit for no default |
unit | no | Piece, Hour, Kilogram, Liter, Meter, … (enum name) — defaults to Piece |
customUnitId | no | Id 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
6 endpoints
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 Created→UnitDto400 invalid_argument→ label missing/too long, or unknowneInvoiceCode
PATCH
/api/units/{id}
Update a custom unit (sparse)
Only the fields you send are changed.
Responses
200 OK→ updatedUnitDto400 invalid_argument→ invalid label or code404 not_found→ not yours
DELETE
/api/units/{id}
Delete a custom unit
Responses
204 No Content→ deleted409 unit_in_use→ still referenced by an item/invoice/Gutschrift/project; cannot delete404 not_found→ not yours
Offers (standalone)
5 endpoints
GET
/api/offers?page=1&limit=50
List all offers (paginated)
Returns all your offers, newest first — standalone ones
(projectId = null) and project-bound ones in one list. Drafts carry a
DRAFT-… number; the real number is allocated on finalize.
GET /api/offers/{id} returns a single offer.
Response 200
{
"items": [
{
"id": 12, "projectId": null, "customerId": 42, "offerNumber": "OFF-2026-07-0012",
"title": "Netzwerkmodernisierung Bürogebäude", "issueDate": "2026-07-03T00:00:00Z",
"validUntil": "2026-08-03T00:00:00Z", "deliveryTimeInDays": 21,
"isFinalized": true, "finalizedAt": "2026-07-03T09:14:00Z",
"pdfUrl": "/api/offers/12/pdf", "archivedPdfHash": "9f2c4a…",
"items": [
{ "id": 31, "itemKey": "NET-01", "description": "Netzwerkinstallation Büro EG", "quantity": 1, "unit": 1, "customUnitId": null, "pricePerUnit": 2400.00, "sortOrder": 0 }
]
}
],
"page": 1, "limit": 50, "total": 1
}
403 plan_forbidden if the plan lacks documents access.
POST
/api/offers
Create a standalone offer (finalize or draft)
Creates an offer with its own customer and line items — no project needed.
With "finalize": true (default) the offer number is allocated and a
hash-sealed PDF is archived in one call; with false you get an editable
draft. Required: customerId, invoiceTemplateId,
items[] (each needs a description; quantity defaults to 1).
Supports Idempotency-Key.
Request body
{
"customerId": 42,
"invoiceTemplateId": 1,
"issueDate": "2026-07-03T00:00:00Z",
"validUntil": "2026-08-03T00:00:00Z",
"deliveryTimeInDays": 21,
"title": "Netzwerkmodernisierung Bürogebäude",
"description": "Erneuerung der Verkabelung und Switches im Erdgeschoss.",
"introductionText": "Vielen Dank für Ihre Anfrage — gerne unterbreiten wir Ihnen folgendes Angebot.",
"footerText": "**Zahlungsbedingungen:** 50% bei Auftragserteilung, 50% nach Abnahme.",
"items": [
{ "itemKey": "NET-01", "description": "Netzwerkinstallation Büro EG", "quantity": 1, "unit": 1, "pricePerUnit": 2400.00 },
{ "itemKey": "NET-03", "description": "Verlegung CAT-7 Kabel", "quantity": 120, "unit": 4, "pricePerUnit": 3.50 }
],
"finalize": true
}
Responses
201 Created→OfferDto404 not_found→ customer/template not yours400 invalid_body/invalid_argument/invalid_state403 plan_forbidden
POST
/api/offers/{id}/finalize
Finalize an offer draft
Allocates the next sequential offer number, renders and archives the PDF (SHA-512). The offer becomes immutable. Project-bound drafts snapshot the project's customer, title and items at this moment.
400 invalid_state if already finalized or empty; 404 not_found if not yours.
DELETE
/api/offers/{id}
Delete an offer draft
Deletes a draft. Finalized offers cannot be deleted via the API.
204 No Content; 400 invalid_state if finalized; 404 not_found.
GET
/api/offers/{id}/pdf
Download the offer PDF
Finalized → the archived, hash-verified PDF. Draft → a fresh render (watermarked if the plan displays watermarks).
Orders
2 endpoints
GET
/api/orders?page=1&limit=50
List all orders (paginated)
Returns all your orders (incoming customer orders), newest first.
GET /api/orders/{id} returns a single order. Requires a plan with
EnableOrders, otherwise 403 plan_forbidden.
POST
/api/orders
Create an order (finalize or draft)
Records an incoming customer order. Finalizing renders and archives the order
confirmation PDF with the next order number (prefix
BusinessInfo.OrderPrefix, default ORD).
customerReference is the customer's own order number.
Optional offerId chains the order to one of your offers.
Supports Idempotency-Key.
Request body
{
"customerId": 42,
"invoiceTemplateId": 1,
"issueDate": "2026-07-03T00:00:00Z",
"customerReference": "BEST-2026-0815",
"expectedDeliveryDate": "2026-07-24T00:00:00Z",
"offerId": 12,
"title": "Netzwerkmodernisierung Bürogebäude",
"description": "Beauftragung gemäß Angebot OFF-2026-07-0012.",
"introductionText": "Vielen Dank für Ihre Bestellung — hiermit bestätigen wir folgende Positionen.",
"footerText": "**Lieferung:** frei Haus. **Zahlungsziel:** 14 Tage netto.",
"items": [
{ "itemKey": "NET-01", "description": "Netzwerkinstallation Büro EG", "quantity": 1, "unit": 1, "pricePerUnit": 2400.00 },
{ "itemKey": "NET-02", "description": "Managed Switch 24-Port inkl. Konfiguration", "quantity": 2, "unit": 1, "pricePerUnit": 450.00 }
],
"finalize": true
}
Responses
201 Created→OrderDto{ id, orderNumber, customerReference, isFinalized, pdfUrl, items[] }404 not_found→ customer/template/offer not yours400 invalid_body/invalid_argument/invalid_state403 plan_forbidden
Also available: POST /api/orders/{id}/finalize,
DELETE /api/orders/{id} (drafts only — confirmed orders cannot be deleted) and
GET /api/orders/{id}/pdf (archived confirmation or watermarked draft render).
Delivery Notes (standalone)
2 endpoints
GET
/api/delivery-notes?page=1&limit=50
List all delivery notes (paginated)
Returns all your delivery notes, newest first — standalone, order-chained
(orderId set) and project-bound. GET /api/delivery-notes/{id}
returns a single note. Same paging and error shape as offers.
POST
/api/delivery-notes
Create a standalone delivery note (finalize or draft)
Creates a delivery note with its own customer and items. Classic notes use
"showPrices": false (positions + quantities only). Optional
orderId chains the note to one of your orders. finalize
defaults to true. Supports Idempotency-Key.
Request body
{
"customerId": 42,
"invoiceTemplateId": 1,
"issueDate": "2026-07-03T00:00:00Z",
"deliveryDate": "2026-07-05T00:00:00Z",
"showPrices": false,
"title": "Netzwerkmodernisierung Bürogebäude",
"introductionText": "Anbei die Lieferung zu Ihrer Bestellung BEST-2026-0815.",
"footerText": "Bitte prüfen Sie die Lieferung auf Vollständigkeit.",
"items": [
{ "itemKey": "NET-02", "description": "Managed Switch 24-Port", "quantity": 2, "unit": 1 },
{ "itemKey": "NET-03", "description": "CAT-7 Kabelrolle 100m", "quantity": 5, "unit": 1 }
],
"orderId": null,
"finalize": true
}
Also available: POST /api/delivery-notes/{id}/finalize,
DELETE /api/delivery-notes/{id} (drafts only) and
GET /api/delivery-notes/{id}/pdf — same semantics as the offer endpoints.
Purchase Orders
2 endpoints
GET
/api/purchase-orders?page=1&limit=50
List all purchase orders (paginated)
Returns all your purchase orders (outgoing orders to suppliers), newest first.
GET /api/purchase-orders/{id} returns a single one. Requires a plan
with EnableOrders, otherwise 403 plan_forbidden.
POST
/api/purchase-orders
Create a purchase order (finalize or draft)
Records an outgoing order to a supplier. The supplier is one of your customer records
(supplierId). Finalizing renders and archives the purchase order PDF with
the next number (prefix BusinessInfo.PurchaseOrderPrefix, default PO).
deliveryAddress is optional — your business address is the default delivery
target. Supports Idempotency-Key.
Request body
{
"supplierId": 42,
"invoiceTemplateId": 1,
"issueDate": "2026-07-03T00:00:00Z",
"expectedDeliveryDate": "2026-07-17T00:00:00Z",
"deliveryAddress": "Lager Süd, Industriestraße 42, 1230 Wien",
"title": "Materialbestellung Netzwerkprojekt",
"description": "Hardware für das Projekt Netzwerkmodernisierung Bürogebäude.",
"introductionText": "Hiermit bestellen wir folgende Positionen zu den vereinbarten Konditionen.",
"footerText": "**Lieferbedingung:** frei Haus. Bitte Bestellnummer auf Lieferschein und Rechnung angeben.",
"items": [
{ "itemKey": "HW-01", "description": "Managed Switch 24-Port", "quantity": 2, "pricePerUnit": 380.00 },
{ "itemKey": "HW-02", "description": "CAT-7 Kabelrolle 100m", "quantity": 5, "pricePerUnit": 89.90 }
],
"finalize": true
}
Responses
201 Created→PurchaseOrderDto{ id, purchaseOrderNumber, supplierId, isFinalized, pdfUrl, items[] }404 not_found→ supplier/template not yours400 invalid_body/invalid_argument/invalid_state403 plan_forbidden
Also available: POST /api/purchase-orders/{id}/finalize,
DELETE /api/purchase-orders/{id} (drafts only) and
GET /api/purchase-orders/{id}/pdf (archived or watermarked draft render).
Self-Billing
4 endpoints
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, 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"
}POST
/api/gutschriften/preview
See the self-bill before you issue it — nothing is persisted
Dry run of the call above. Identical body, returns application/pdf — no Gutschrift row,
no GUT number consumed, nothing archived. The number reads
GUT-2026-05-PREVIEW.
The payout QR is the one thing that matters here: on a real Gutschrift it carries an EPC transfer to your supplier's IBAN. On the preview it points at timelane instead, so a preview can never be scanned and paid. Position and size are unchanged, so the layout still matches.
curl -X POST https://timelane.cloud/api/gutschriften/preview \
-H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
-H "Content-Type: application/json" \
-d '{"customerId":47,"invoiceTemplateId":1,"items":[{"itemKey":"AFF-01","description":"Vermittlungsprovision","quantity":14,"unit":"Piece","pricePerUnit":45}]}' \
-o preview.pdf
Same errors as the real call: 404 not_found, 400 invalid_argument / invalid_body, 422 business_info_missing, 403 plan_forbidden.
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",
"archivedPdfHash": "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
}Flat-Rate Projects
17 endpoints
A flat-rate project bundles line items and turns them into a finalized offer,
delivery note and invoice. All endpoints in this group require a plan with
flat-rate project 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, "surchargePercent": 5, "effectivePrice": 2520.00, "sortOrder": 0 },
{ "id": 81, "description": "Frontend implementation", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 4000.00, "surchargePercent": null, "effectivePrice": 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
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | int | yes | Must belong to you |
name | string | yes | |
description | string | no | |
totalPrice | decimal | no | Used by ProjectPrice / Combined strategies |
pricingStrategy | enum | no | ProjectPrice (default), ItemPrice, Combined |
hideItemPrices | bool | no | Hide per-item prices on documents |
items | array | no | Each: description (req), quantity (def 1), unit (def Piece), customUnitId, price, surchargePercent (internal margin, > -100), 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 Created→PaushalProjectDto(see GET /{id})400 invalid_argument→ name missing404 not_found→ customer not yours403 plan_forbidden→ plan lacks flat-rate project 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→ updatedPaushalProjectDto404 not_found→ not yours
GET
/api/paushal-projects/{id}/items
List a project's items
Items sorted by sortOrder. surchargePercent is an internal margin on top of
price (5 = +5%, negative = discount); effectivePrice is the price with the
surcharge applied, rounded to cents. Documents created from the project (offer, delivery note,
invoice) only ever show the effective price — the surcharge never appears on any document.
Response 200
[
{ "id": 80, "description": "UX concept & wireframes", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 2400.00, "surchargePercent": 5, "effectivePrice": 2520.00, "sortOrder": 0 },
{ "id": 81, "description": "Frontend implementation", "quantity": 1, "unit": "Piece", "customUnitId": null, "price": 4000.00, "surchargePercent": null, "effectivePrice": 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. Optional surchargePercent adds an internal margin (must be > -100). Supports Idempotency-Key.
Example request
{ "description": "CMS migration", "quantity": 1, "unit": "Piece", "price": 2000.00, "surchargePercent": 5 }
Responses
201 Created→PaushalProjectItemDto400 invalid_argument→ description missing or surchargePercent ≤ -100404 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, surchargePercent, sortOrder.
Send surchargePercent: 0 to remove an existing surcharge; sending unit
without customUnitId clears a previously set custom unit.
Responses
200 OK→ updatedPaushalProjectItemDto400 invalid_argument→ surchargePercent ≤ -100404 not_found→ item/project not yours
DELETE
/api/paushal-projects/{id}/items/{itemId}
Delete a project item
Responses
204 No Content→ deleted404 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→ reordered400 invalid_body→ itemIds empty404 not_found→ project not yours
POST
/api/paushal-projects/{id}/offer
Create & finalize the offer
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.
POST
/api/paushal-projects/{id}/offer/preview
See the offer before you issue it — nothing is persisted
Dry run of the call above. Identical body, returns application/pdf with the project's
current snapshot (customer, title, items) rendered exactly as finalize would — and persists nothing:
no offer row, not even a draft, no offer number consumed, nothing archived. The number
reads OFF-2026-08-PREVIEW.
Same guards as the real call, including the 1:1 rule — 400 invalid_state once the project already has a finalized offer.
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
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"
}POST
/api/paushal-projects/{id}/delivery-note/preview
See the delivery note before you issue it — nothing is persisted
Dry run of the call above. Identical body, returns application/pdf — no delivery note row,
no number consumed, nothing archived. The number reads
DN-2026-08-PREVIEW. Same guards as the real call, including
400 invalid_state once the project already has a finalized delivery note.
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 Created→InvoiceDto(same shape as POST /api/invoices)400 invalid_state→ project has no priced items to invoice404 not_found→ project/template not yours
POST
/api/paushal-projects/{id}/invoices/preview
See the project invoice before you issue it — nothing is persisted
Dry run of the call above. Identical body, returns application/pdf with the line items
the project's pricing strategy would produce — and persists nothing: no invoice row, no invoice number
consumed, no Stripe Payment Link, nothing archived. The number reads
INV-2026-05-PREVIEW, the payment QR points at timelane and no e-invoice XML is embedded.
Same errors as the real call: 400 invalid_state, 404 not_found, 403 plan_forbidden.