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.

StatusFehlercodeBedeutung
401missing_api_keyHeader nicht mitgeschickt
401invalid_api_keySchlüssel ungültig oder widerrufen
403plan_forbiddenTarif 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.

Trifft derselbe Schlüssel ein, während die erste Anfrage noch läuft — etwa weil Sie nach einem Zeitüberschreitungsfehler sofort wiederholen —, antworten wir mit 409 idempotency_key_in_progress. Warten Sie das Ende der ersten Anfrage ab und wiederholen Sie dann mit demselben Schlüssel; Sie erhalten deren Antwort.

Endet die Verarbeitung, bevor eine Antwort gespeichert wurde — etwa weil unser Dienst mitten in Ihrer Anfrage neu startet —, verfällt die Reservierung nach zehn Minuten. Die nächste Wiederholung mit demselben Schlüssel läuft dann normal durch.

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.

Fremde Bereiche ansprechen

Ein Schlüssel gehört einem Bereich und spricht standardmäßig genau diesen an. Sind Sie Gast in einem anderen Bereich, können Sie ihn mit dem Header X-Timelane-Tenant ansprechen — Wert ist die Bereichs-ID.

curl https://timelane.cloud/api/times?settled=false \
  -H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
  -H "X-Timelane-Tenant: 17"

Mitgliedschaft und Tarife beider Seiten werden bei jeder Anfrage neu geprüft: ein Schlüssel lebt lange, eine Einladung nicht. Wird sie zurückgenommen oder fällt ein Tarif weg, ist die Tür sofort zu.

StatusFehlercodeBedeutung
400invalid_tenantHeader ist keine Zahl
404tenant_not_foundKeine Mitgliedschaft — oder sie wurde entfernt
403tenant_forbiddenEinladung nicht angenommen, oder ein Tarif fehlt

Nur die Leseendpunkte für Kunden, Projekte, Aufgaben und Zeiten folgen dem Header. Alle übrigen Endpoints sprechen weiter den Bereich des Schlüsselinhabers an und liefern unter dem Header nichts, statt in einen fremden Bereich zu greifen. Angelegt und geändert wird ausschließlich im eigenen Bereich.

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.

Einrichten

Was einmal steht und danach jeden Beleg trägt: Absender, Vorlagen, Katalog.

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",
  "taxNumber": "46 123/4567",
  "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.

taxNumber (max 50 characters) is the tax number issued by the tax office. It is optional and matters for sellers without a vatId, such as small businesses: it is printed on documents and written into the e-invoice as the seller's tax registration (BT-32). With neither set, the e-invoice names no tax registration.

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" }

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.",
  "cashDiscountPercent": 2.0,
  "cashDiscountDays": 10,
  "eInvoiceProfile": "EN16931",
  "businessProcessId": null,
  "mailTemplateId": 3,
  "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
taxModeenumnoNoTax, 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).
zeroTaxCategorystringnoZ (zero rated, default), E (exempt, e.g. small business), AE (reverse charge), K (intra-community supply), G (export), O (not subject to VAT) — EN16931 semantics of 0 %-rated lines
taxExemptionReasonstringno*BT-120 exemption text, max 500 chars, printed on the PDF. Required for E, AE, K, G and O unless autoTaxNotice is true — enforced at template create/update, not at finalize.
autoTaxNoticeboolnoDefault false. true prints the statutory notice with its paragraph instead of taxExemptionReason — worded by the issuer's country (AT/DE) and small-business status (business data) and the document language — and writes the matching VATEX code to BT-121. Other issuer countries keep taxExemptionReason.
autoTaxCaseboolnoDefault false. true works out each line's VAT case from the issuer's country and small-business status, the customer's country, isConsumer and VAT id, and the line's supplyKind: domestic → the template rate; EU business with VAT id → AE (service) or K (goods); EU private → the template rate; outside the EU → G (goods) or O (service to a business). Issuers outside AT/DE keep the template's settings. Changes amounts — preview first.
taxRatedecimalno0..100 percent (default 20); the default rate for lines without an explicit one
taxLabelstringnoe.g. "USt", "VAT"; max 50 chars
paymentQrModeenumnoNone, EpcQrCode, StripePaymentLink. EpcQrCode requires the EPC plan feature
paymentInstructionsstringnoMax 500 chars. Printed on the PDF and appended to BT-20 in the e-invoice
cashDiscountPercent / cashDiscountDaysdecimal / intnoSkonto preset for new invoices: 0.01–99.99 % (max 2 decimals) within 1–365 days of the issue date, always on the whole gross amount. Both or neither. On PATCH a missing half keeps its value, cashDiscountPercent: 0 removes the discount
eInvoiceProfileenumnoNone, Basic, EN16931, XRechnung. Anything but None requires the e-invoice plan feature
businessProcessIdstringnoBT-23 ProfileID URN; max 200 chars
mailTemplateIdintnoThe mail template invoices on this document template are sent with. Omit it, or send 0 on PATCH, for Timelane's built-in wording. 404 not_found if the id is not one of yours. Replaces mailTitle / mailContent, which are no longer read — mail text lives on its own entity now, with {Platzhalter} instead of %Platzhalter%. Existing wording was migrated automatically.
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 Created → InvoiceTemplateDetailDto (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

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

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,
      "supplyKind": "Service",
      "createdAt": "2026-05-02T09:14:00Z",
      "updatedAt": null
    },
    {
      "id": 8,
      "itemKey": "DESIGN-01",
      "description": "UI/UX design",
      "defaultPrice": 85.00,
      "unit": "Hour",
      "customUnitId": null,
      "supplyKind": "Service",
      "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,
  "supplyKind": "Service",
  "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,
  "supplyKind": "Service"
}
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
supplyKindnoService (default) or Goods. Handed to a document line added from the catalogue in the app, where it feeds a template's autoTaxCase.

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,
  "supplyKind": "Service",
  "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,
  "supplyKind": "Service",
  "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 → UnitDto
  • 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

Arbeiten

Die Kette vom Kunden bis zur gebuchten Stunde — jede Ebene hängt an der darüber.

Customers

11 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

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",
  "salutation": "Sehr geehrte Frau Berger"
}

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",
  "eInvoiceProfile": "XRechnung",
  "salutation": "Sehr geehrte Frau Berger"
}
FieldRequiredNotes
nameyesThe only mandatory field
salutationnoOpening line of letters to this customer. Fills {Kunde.Anrede} in mail templates and reminder texts; empty falls back to „Sehr geehrte Damen und Herren"
isConsumernotrue private person, false business, omitted/null decided by the VAT id (with one: business). Used by templates with autoTaxCase.
isSmallBusinessnoThe customer bills under the small-business scheme. Decides the exemption notice on self-billed credit notes (Gutschriften) to this customer. Default false.
vatIdnoUnique per account when set; omit for B2C / customers without a VAT ID
addressno
countrynoISO 3166-1 alpha-2 (e.g. AT) when set
eInvoiceProfilenoNone, Basic, EN16931 or XRechnung — the format this recipient accepts. Omit it and the template decides, which is how it always worked.
othersnoEmpty string → null

eInvoiceProfile sits on the customer because the format depends on what the buyer's system accepts, while the tax case (domestic, intra-EU, export) sits on the template. Kept apart, three tax templates stay three; folded together, every profile multiplies them. A single invoice can still override both — see POST /api/invoices.

POST /api/customers/{id}/contacts Address a customer's mailboxes by role

A customer can hold any number of e-mail addresses, each tagged with what it is for. The email field on the customer itself is the default billing address of this list — send only that one and nothing changes for you.

Three tags are resolved by name; everything else you invent is equally valid and simply yours to route with.

TagMeaning
rechnungWhere invoices go
mahnungWhere reminders go
e-rechnungBT-49, the electronic address the recipient's system takes delivery at. Not necessarily a mailbox a person reads — which is why it is separate from the contact printed on the PDF. Unset, BT-49 falls back to the billing address.

Request body

{
  "email": "edi@acme.example",
  "displayName": "Rechnungseingang EDI",
  "tags": ["e-rechnung"],
  "makeDefault": false
}

Responses

  • 201 Created → { "id": 7, "email": "edi@acme.example", "displayName": "Rechnungseingang EDI", "isDefaultBilling": false, "tags": ["e-rechnung"] }
  • 409 contact_exists → the customer already holds that address
  • 404 not_found → customer not yours

Also available: GET /api/customers/{id}/contacts, PATCH /api/customers/{id}/contacts/{contactId} (an empty tags array clears the tags), POST …/contacts/{contactId}/default and DELETE …/contacts/{contactId}. Deleting the default hands the role to the oldest remaining address, so a customer with contacts always has a fallback.

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"
}

Projects

4 endpoints
GET /api/projects?includeShared=true&page=1&limit=50 List projects, own and shared, in one list

Projects of the addressed workspace together with the projects other workspaces have shared into it. The source property tells them apart — own or shared. A shared row is a read window onto one project: it carries the project name, its customer's name and who shared it, never the sharing workspace's description, status or figures.

Query parameters

NameTypeRequiredNotes
customerIdintnoOwn projects only — a shared project's customer belongs to the sharing workspace
searchstringnoSubstring of the project name
includeSharedboolnoDefault true
pageintno1-based, default 1
limitintno1..200, default 50

Response 200

{
  "items": [
    { "id": 118, "name": "Umbau Trafostation Nord", "description": "Erneuerung der Mittelspannungsverteilung", "customerId": 42, "customerName": "Stadtwerke Klosterneuburg", "projectType": "TimeTracked", "projectStatus": "Accepted", "totalPrice": null, "completedAt": null, "source": "own", "sourceTenantId": null, "sourceTenantName": null },
    { "id": 204, "name": "Elektroplanung Halle 3", "description": null, "customerId": null, "customerName": "Ziegelwerk Hartl GmbH", "projectType": "", "projectStatus": "", "totalPrice": null, "completedAt": null, "source": "shared", "sourceTenantId": 17, "sourceTenantName": "Elektro Sailer e.U." }
  ],
  "page": 1,
  "limit": 50,
  "total": 2
}
GET /api/projects/{id} Fetch one project

Resolves a project of the addressed workspace, or one shared into it. Returns 404 not_found for anything else — including a project that exists in a workspace you have no window onto.

Response 200

{ "id": 118, "name": "Umbau Trafostation Nord", "description": "Erneuerung der Mittelspannungsverteilung", "customerId": 42, "customerName": "Stadtwerke Klosterneuburg", "projectType": "TimeTracked", "projectStatus": "Accepted", "totalPrice": null, "completedAt": null, "source": "own", "sourceTenantId": null, "sourceTenantName": null }
POST /api/projects Create a time-tracked project

Creates a project in your own workspace. Reads answer for the workspace named by X-Timelane-Tenant; writes do not — a project is structure, and there is no permission that would let a guest add one.

Flat-rate projects carry pricing, milestones and offer fields and are created through their own endpoints. What this creates is always TimeTracked.

Body

NameTypeRequiredNotes
namestringyesUp to 100 characters
customerIdintyesMust be one of your customers
descriptionstringnoUp to 500 characters
{
  "name": "Umbau Trafostation Nord",
  "customerId": 42,
  "description": "Erneuerung der Mittelspannungsverteilung"
}

Response 201

{ "id": 118, "name": "Umbau Trafostation Nord", "description": "Erneuerung der Mittelspannungsverteilung", "customerId": 42, "customerName": "Stadtwerke Klosterneuburg", "projectType": "TimeTracked", "projectStatus": "Created", "totalPrice": null, "completedAt": null, "source": "own", "sourceTenantId": null, "sourceTenantName": null }

Errors

StatusCodeWhen
400invalid_argumentName missing, or a field over its length
404not_foundThe customer is not yours
PATCH /api/projects/{id} Rename, re-assign, close or reopen a project

Every field is optional; the ones you leave out stay as they are. Your own projects only, for the same reason as above.

Body

NameTypeNotes
namestringUp to 100 characters, not empty
descriptionstringUp to 500 characters; empty clears it
customerIdintMoves the project to another of your customers
completedbooltrue closes the project, false reopens it
{ "name": "Umbau Trafostation Nord (Bauteil 2)", "completed": true }

Response 200

The project as GET /api/projects/{id} returns it.

Tasks

4 endpoints
GET /api/tasks?projectId=&page=1&limit=50 List tasks — what hours are booked against

Own tasks only. A shared project is a window onto that project and its name; its tasks belong to the sharing workspace and stay there.

Query parameters

NameTypeRequiredNotes
projectIdintnoRestrict to one project
searchstringnoSubstring of the task name
pageintno1-based, default 1
limitintno1..200, default 50

Response 200

{
  "items": [
    { "id": 305, "name": "Verkabelung Trafostation", "description": "Zuleitungen und Verteiler", "hourlyRate": 95.00, "projectId": 118, "projectName": "Umbau Trafostation Nord" },
    { "id": 306, "name": "Dokumentation", "description": "Schemata und Prüfprotokolle", "hourlyRate": 70.00, "projectId": 118, "projectName": "Umbau Trafostation Nord" }
  ],
  "page": 1,
  "limit": 50,
  "total": 2
}
GET /api/tasks/{id} Fetch one task

Response 200

{ "id": 305, "name": "Verkabelung Trafostation", "description": "Zuleitungen und Verteiler", "hourlyRate": 95.00, "projectId": 118, "projectName": "Umbau Trafostation Nord" }
POST /api/tasks Create a task under one of your projects

Your own projects only — the same split as for projects themselves. A shared project's tasks belong to the workspace that shared it.

Body

NameTypeRequiredNotes
namestringyesUp to 100 characters
projectIdintyesMust be one of your projects
hourlyRatedecimalyes0 is a real choice and means "no rate"
descriptionstringnoUp to 500 characters
{
  "name": "Verkabelung Trafostation",
  "projectId": 118,
  "hourlyRate": 95.00,
  "description": "Zuleitungen und Verteiler"
}

Response 201

{ "id": 305, "name": "Verkabelung Trafostation", "description": "Zuleitungen und Verteiler", "hourlyRate": 95.00, "projectId": 118, "projectName": "Umbau Trafostation Nord" }
PATCH /api/tasks/{id} Rename a task, change its rate or move it

A new rate applies from here on. Hours already invoiced keep the money they were billed at — that figure sits on the invoice, not on the task.

Body

NameTypeNotes
namestringUp to 100 characters, not empty
descriptionstringUp to 500 characters; empty clears it
hourlyRatedecimalNot negative
projectIdintMoves the task to another of your projects
{ "hourlyRate": 105.00 }

Response 200

The task as GET /api/tasks/{id} returns it.

Times

4 endpoints
GET /api/times?from=&to=&billed=&settled= List booked hours

Booked hours of the addressed workspace, newest first. Two independent settlement flags, answering different questions about the same hour: billed / invoiceId is whether the customer has been charged for it, settled / settledGutschriftId is whether the person who booked it has been paid. Both can be true — a workspace that invoices its client at one rate and settles with the contractor at another does exactly that.

Query parameters

NameTypeRequiredNotes
projectIdintnoRestrict to one project
taskIdintnoRestrict to one task
fromdatenoInclusive, by start time
todatenoInclusive; a bare date covers that whole day
createdByUserIdintnoOnly hours booked by this person — the payout view
billedboolnoOmit for both
settledboolnoOmit for both
pageintno1-based, default 1
limitintno1..200, default 50

Response 200

{
  "items": [
    { "id": 8801, "taskId": 305, "taskName": "Verkabelung Trafostation", "projectId": 118, "projectName": "Umbau Trafostation Nord", "startTime": "2026-08-11T08:00:00Z", "endTime": "2026-08-11T13:30:00Z", "durationMinutes": 300, "pauseMinutes": 30, "description": "Zuleitungen gezogen", "createdByUserId": 17, "isBilled": true, "invoiceId": 4410, "settledGutschriftId": null, "settledAt": null },
    { "id": 8794, "taskId": 306, "taskName": "Dokumentation", "projectId": 118, "projectName": "Umbau Trafostation Nord", "startTime": "2026-08-10T09:00:00Z", "endTime": "2026-08-10T11:00:00Z", "durationMinutes": 120, "pauseMinutes": 0, "description": "Schemata aktualisiert", "createdByUserId": 17, "isBilled": false, "invoiceId": null, "settledGutschriftId": 77, "settledAt": "2026-08-21T09:26:10Z" }
  ],
  "page": 1,
  "limit": 50,
  "total": 2
}
GET /api/times/{id} Fetch one time entry

Response 200

{ "id": 8801, "taskId": 305, "taskName": "Verkabelung Trafostation", "projectId": 118, "projectName": "Umbau Trafostation Nord", "startTime": "2026-08-11T08:00:00Z", "endTime": "2026-08-11T13:30:00Z", "durationMinutes": 300, "pauseMinutes": 30, "description": "Zuleitungen gezogen", "createdByUserId": 17, "isBilled": true, "invoiceId": 4410, "settledGutschriftId": null, "settledAt": null }
POST /api/times Book hours against a task

The one write in this API that reaches into somebody else's workspace. Booking hours is what a guest is invited to do, so this endpoint answers for the workspace named by X-Timelane-Tenant — provided the role behind your key carries Zeiten erfassen on that very task. Seeing a task is not permission to book against it.

The hour belongs to the workspace; the authorship belongs to your key. That difference is what later separates the customer's invoice from the contractor's credit note, so createdByUserId is not part of the payload.

Body

NameTypeRequiredNotes
taskIdintyesA task of the addressed workspace
startTimedatetimeyesRead as UTC
endTimedatetimeyesMust be after startTime
pauseMinutesintnoUnpaid minutes inside the entry, default 0. Must be shorter than the entry; durationMinutes in the response is net of it
descriptionstringnoUp to 500 characters
{
  "taskId": 305,
  "startTime": "2026-08-11T08:00:00Z",
  "endTime": "2026-08-11T13:30:00Z",
  "pauseMinutes": 30,
  "description": "Zuleitungen gezogen"
}

Response 201

{ "id": 8801, "taskId": 305, "taskName": "Verkabelung Trafostation", "projectId": 118, "projectName": "Umbau Trafostation Nord", "startTime": "2026-08-11T08:00:00Z", "endTime": "2026-08-11T13:30:00Z", "durationMinutes": 300, "pauseMinutes": 30, "description": "Zuleitungen gezogen", "createdByUserId": 17, "isBilled": false, "invoiceId": null, "settledGutschriftId": null, "settledAt": null }

Errors

StatusCodeWhen
400invalid_argumentendTime not after startTime, pauseMinutes negative or not shorter than the entry, or description too long
403forbiddenYour role does not allow booking against this task
404not_foundNo such task in the addressed workspace
PATCH /api/times/{id} Correct times or the description

Needs Zeiten bearbeiten on the entry's own task. The task itself cannot be changed: moving an hour elsewhere would move it past the permission that allowed it, and past the invoice that may already carry it.

An invoiced entry is frozen. It is already on a document, and changing it would leave that document claiming hours the timesheet no longer shows.

Body

NameTypeNotes
startTimedatetimeRead as UTC
endTimedatetimeMust end after the start, whichever of the two you send
pauseMinutesintOmit to keep the stored pause. Checked against the resulting entry, so shortening an entry below its pause fails too
descriptionstringUp to 500 characters; empty clears it
{ "endTime": "2026-08-11T14:00:00Z", "pauseMinutes": 45 }

Errors

StatusCodeWhen
400invalid_argumentEnd not after start, pauseMinutes negative or not shorter than the entry, or description too long
403forbiddenYour role does not allow editing this task's hours
409already_billedThe entry is on an invoice

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

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, 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 missing
  • 404 not_found → customer not yours
  • 403 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 → updated PaushalProjectDto
  • 404 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 → PaushalProjectItemDto
  • 400 invalid_argument → description missing or surchargePercent ≤ -100
  • 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, surchargePercent, sortOrder. Send surchargePercent: 0 to remove an existing surcharge; sending unit without customUnitId clears a previously set custom unit.

Responses

  • 200 OK → updated PaushalProjectItemDto
  • 400 invalid_argument → surchargePercent ≤ -100
  • 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

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 invoice
  • 404 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.

Reports

1 endpoint
GET /api/reports/guest/{guestUserId}/pdf Statement of one guest's hours

The Leistungsnachweis for every hour one guest booked in the addressed workspace, across all customers and projects, as a PDF — the same document the interface produces under Berichte.

The rows behind it are available as JSON from /api/times?createdByUserId=: use that one to reconcile, this one to file or to hand over.

Amounts follow the same rules as every other statement: a task without a rate prints no money, nor does a flat-rate project, everything else does. Whether a rate applies is decided by the data, not by this endpoint.

Query parameters

NameTypeRequiredNotes
fromdatenoInclusive, by start time. Default: one month back
todatenoInclusive; a bare date covers that whole day. Default: today
detailsboolnoList every booking instead of one sum per task. Default false
curl "https://timelane.cloud/api/reports/guest/17/pdf?from=2026-08-01&to=2026-08-31&details=true" \
  -H "X-Api-Key: qsp_live_a8f3e2c1b9d4e6f7g8h9i0j1k2l3m4n5" \
  -o leistungsnachweis-august.pdf

Response 200

application/pdf. A guest with no hours in the period yields an empty statement, not a 404.

Response 400

{ "error": "no_template", "message": "Für diesen Bereich ist keine Rechnungsvorlage hinterlegt." }

Belegen

Was daraus hinausgeht, in der Reihenfolge, in der es entsteht.

Offers (standalone)

6 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 → OfferDto
  • 404 not_found → customer/template not yours
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 plan_forbidden
POST /api/offers/preview Preview an offer without creating it

Same body as POST /api/offers, same validation, same errors — but the response is the rendered PDF and nothing is stored: no offer row, no consumed offer number, nothing archived. finalize is ignored here.

The offer number on the page reads OFF-2026-07-PREVIEW (your configured prefix, current year and month), because the number circle stays untouched. Everything else — positions, totals, tax block, layout — is the document POST /api/offers would produce for the same body.

curl -X POST https://timelane.cloud/api/offers/preview \
  -H "X-Api-Key: qsp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"customerId":42,"invoiceTemplateId":1,"title":"Netzwerkmodernisierung Bürogebäude","items":[{"description":"Netzwerkinstallation Büro EG","quantity":1,"pricePerUnit":2400.00}]}' \
  -o preview.pdf

Responses

  • 200 OK → application/pdf
  • 404 not_found → customer/template not yours
  • 422 business_info_missing → configure your business details first
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 plan_forbidden

No Idempotency-Key needed — a preview stores nothing, so there is nothing to repeat.

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

3 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 yours
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 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).

POST /api/orders/preview Preview an order confirmation without creating it

Same body as POST /api/orders, same validation, same errors — but the response is the rendered order confirmation and nothing is stored: no order row, no consumed order number, nothing archived. finalize is ignored here.

The number on the page reads ORD-2026-07-PREVIEW, because the number circle stays untouched; everything else matches the document the real call produces.

curl -X POST https://timelane.cloud/api/orders/preview \
  -H "X-Api-Key: qsp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"customerId":42,"invoiceTemplateId":1,"customerReference":"BEST-2026-0815","items":[{"description":"Managed Switch 24-Port","quantity":2,"pricePerUnit":450.00}]}' \
  -o preview.pdf

Responses

  • 200 OK → application/pdf
  • 404 not_found → customer/template/offer not yours
  • 422 business_info_missing → configure your business details first
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 plan_forbidden

Delivery Notes (standalone)

3 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.

POST /api/delivery-notes/preview Preview a delivery note without creating it

Same body as POST /api/delivery-notes, same validation, same errors — but the response is the rendered PDF and nothing is stored: no note row, no consumed number, nothing archived. finalize is ignored here, orderId is checked exactly as the real call checks it.

The number on the page reads DEL-2026-07-PREVIEW, because the number circle stays untouched; showPrices renders the same way it will on the issued note.

curl -X POST https://timelane.cloud/api/delivery-notes/preview \
  -H "X-Api-Key: qsp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"customerId":42,"invoiceTemplateId":1,"showPrices":false,"items":[{"description":"Managed Switch 24-Port","quantity":2}]}' \
  -o preview.pdf

Responses

  • 200 OK → application/pdf
  • 404 not_found → customer/template/order not yours
  • 422 business_info_missing → configure your business details first
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 plan_forbidden

Invoices

10 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

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",
  "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.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.",
  "purchaseOrderReference": "4500012345",
  "eInvoiceProfile": "XRechnung",
  "cashDiscountPercent": 2.0,
  "cashDiscountDays": 10,
  "servicePeriodStart": "2026-05-01T00:00:00Z",
  "servicePeriodEnd": "2026-05-15T00:00:00Z",
  "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
    }
  ]
}
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)
customItems[].taxRatenoPer-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[].taxCategorynoEN16931 VAT category (S, Z, AE, G, …). Omitted → derived: S for rate > 0, else the template's zeroTaxCategory.
customItems[].supplyKindnoService (default) or Goods. With a template that has autoTaxCase, it decides e.g. intra-community supply (K) versus reverse charge (AE) for an EU business customer, and export (G) outside the EU.
purchaseOrderReferencenoBT-13, the order number the buyer assigned for this invoice. Emitted to the e-invoice only when set. Distinct from BT-10 (buyerReference on the customer).
eInvoiceProfilenoOverrides the format for this one invoice: None, Basic, EN16931, XRechnung. Resolution order is invoice → customer → template, so omit it to use the customer's profile.
cashDiscountPercent / cashDiscountDaysnoSkonto for this invoice. An omitted half comes from the template; cashDiscountPercent: 0 issues without Skonto. The period must end before dueDate, otherwise 400 invalid_argument. Printed as a sentence on the PDF and written to BT-20 as #SKONTO#TAGE=10#PROZENT=2.00# (XRechnung BR-DE-18). The detail response carries cashDiscountPercent, cashDiscountDays, cashDiscountDate and cashDiscountAmount; all four are null without Skonto.
servicePeriodStart / servicePeriodEndnoService period (BG-14, BT-73/BT-74), printed on the PDF. Equal dates, or only one of the two, state a single date of service (BT-72). Omitted → no service period is stated and BT-72 carries the issue date. Echoed in the detail response.
deliveryCountrynoISO 3166-1 alpha-2 (DE) or country name. Deliver-to country (BT-80) for an intra-community supply (category K). Omitted → the customer's country, with the customer's address as delivery address. Unknown values are ignored.

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 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/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 previewWhy
Invoice number reads INV-2026-05-PREVIEWYour real prefix and year-month, but no sequence — the number circle is untouched
Payment QR points at timelane.cloudA 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/AA 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", "withCashDiscount": true }

If paidDate is omitted or null, today is used. With withCashDiscount: true the payment is the open amount less the invoice's Skonto; 400 invalid_argument if the invoice has none.

Behind this shortcut sits a payment: it books whatever is still open, so an invoice that already received a partial payment ends up settled without losing what was there. isPaid keeps meaning exactly what it always meant. Full control is in POST /api/invoices/{id}/payments.

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"
}
POST /api/invoices/{id}/payments Record money received — including partial payments

Books a payment against an invoice. Amounts are always positive: a correction is a credit note, not a negative payment. A partial payment is simply an amount smaller than what is open — there is no separate concept and no flag for it. Supports Idempotency-Key.

Request body

{
  "amount": 500.00,
  "date": "2026-08-20T00:00:00Z",
  "reference": "Kontoauszug 148 / RF61 R202 6080 010",
  "withCashDiscount": false
}

withCashDiscount: true books what this payment leaves open as Skonto, up to the discount the invoice offers and has not yet had deducted. Anything beyond that stays open and is dunned as usual.

Responses

  • 201 Created → { "id": 4, "amount": 500.00, "date": "2026-08-20T00:00:00Z", "reference": "Kontoauszug 148 / RF61 R202 6080 010", "source": "Manual", "cashDiscountAmount": 0.00 }
  • 400 invalid_argument → amount is zero or negative, or withCashDiscount on an invoice without Skonto
  • 404 not_found → invoice not yours

The balance

GET /api/invoices/{id}/payments answers with the full picture, which is what a reminder would be based on:

{
  "gross": 1200.00,
  "credited": 0.00,
  "paid": 500.00,
  "open": 700.00,
  "overpayment": 0.00,
  "isSettled": false,
  "paymentReference": "RF61R2026080010",
  "payments": [
    { "id": 4, "amount": 500.00, "date": "2026-08-20T00:00:00Z", "reference": "Kontoauszug 148", "source": "Manual", "cashDiscountAmount": 0.00 }
  ],
  "cashDiscount": 0.00
}

cashDiscount is the Skonto deducted so far; it counts against open like a payment.

DELETE /api/invoices/{id}/payments/{paymentId} removes one again and reopens the invoice if that leaves a remainder.

GET /api/payments/lookup?reference=RF61R2026080010 Resolve a bank statement reference to its invoice

Every invoice carries a structured creditor reference per ISO 11649, derived from its number: R-2026-08-0010 → RF61 R202 6080 010. It is printed in the payment details, encoded in the EPC QR code's structured remittance field, and repeated on every reminder about that invoice — so all three name the same thing and a payer's bank carries it through to the statement unchanged.

Pass what the statement shows (spaces and case are ignored) and get the invoice back. A malformed reference resolves to nothing rather than to the wrong invoice — that is what the two check digits are for.

Response 200

{ "invoiceId": 9, "invoiceNumber": "R-2026-08-0010", "open": 700.00, "isSettled": false }

404 not_found when the reference is invalid or matches no invoice of yours.

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"
    }
  ]
}
FieldRequiredNotes
reasonnoPrinted as introduction text on the document
issueDatenoDefault: today
itemsnoOmit entirely for a full storno (all remaining quantities)
items[].itemKeyyesMust be a line of the original invoice; duplicate keys with the same rate pool their quantities
items[].quantityyesPositive; ≤ remaining quantity (original − already credited, cumulative over all credit notes)
items[].pricePerUnitnoDefault: original line price; must not exceed it (price corrections only downwards)
items[].descriptionnoDefault: original line description
items[].taxRatenoOnly 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

StatusCodeWhen
404not_foundInvoice does not exist / not yours
400invalid_stateInvoice is not finalized
409over_creditRequested quantity/amount exceeds the remaining creditable rest (message names the itemKey and rest)
409partial_not_supportedInvoice has time-based lines or a total-amount override — only full storno (omit items)
400ambiguous_itemitemKey exists with several tax rates — send taxRate as discriminator
400invalid_argumentUnknown 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.

Dunning Notices

5 endpoints
POST /api/mahnungen Issue a reminder — from your own lines or from Timelane invoices

The eighth document kind: own number circle (M-… by default), own archive, immutable once issued. Two ways to fill it, and they can be combined — which is what a partly-migrated ledger produces:

FieldMeaning
invoiceIdsTimelane builds the lines itself and reads what is still open on each invoice, payments and corrections included.
itemsYou supply the lines. Nothing has to exist in Timelane — a reminder about invoices you keep elsewhere is a document like any other.

Request body

{
  "customerId": 42,
  "invoiceTemplateId": 1,
  "title": "1. Mahnung",
  "issueDate": "2026-08-24T00:00:00Z",
  "paymentTermDays": 14,
  "invoiceIds": [9],
  "items": [
    {
      "documentNumber": "EXT-4711",
      "issueDate": "2026-06-01T00:00:00Z",
      "dueDate": "2026-06-15T00:00:00Z",
      "grossAmount": 250.00,
      "paidAmount": 0.00
    }
  ],
  "fee": 40.00,
  "feeKey": "MAHN_1",
  "feeDescription": "Mahnspesen 1. Mahnung",
  "introductionText": "Trotz unserer Zahlungserinnerung ist der folgende Betrag offen.",
  "footerText": "Weitere Verzugszinsen behalten wir uns vor."
}

Responses

  • 201 Created → DunningNoticeDto { noticeNumber, openTotal, fee, total, feeStatus, paymentReference, pdfUrl, items[] }
  • 404 not_found → customer or invoice not yours
  • 422 business_info_missing → configure your business details first
  • 400 invalid_argument → no lines, empty title, negative fee

invoiceTemplateId is required and does what it does everywhere else: font, colour, logo — and the language the reminder is written in. A Spanish customer gets a Spanish reminder from the same rule.

The lines are snapshots: a reminder says what was owed on the day it went out, and an invoice paid next week does not rewrite it. No VAT is shown — a dunning fee is damages, not turnover, so the reminder is not an invoice. There is no reversal either; a reminder sent in error is dealt with by POST /api/mahnungen/{id}/waive-fee, and the document stays.

POST /api/mahnungen/preview Preview a reminder without issuing it

Same body, same validation, same errors — the rendered PDF, nothing stored: no row, no consumed number, nothing archived. The number reads M-2026-08-PREVIEW.

curl -X POST https://timelane.cloud/api/mahnungen/preview \
  -H "X-Api-Key: qsp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"customerId":42,"invoiceTemplateId":1,"title":"1. Mahnung","invoiceIds":[9],"fee":40.00}' \
  -o preview.pdf

Also available: GET /api/mahnungen (paginated), GET /api/mahnungen/{id} and GET /api/mahnungen/{id}/pdf (the archived, hash-verified document).

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
    }
  ]
}
FieldRequiredNotes
customerIdeitherA customer record of yours. Exactly one of customerId / recipientTenantId.
recipientTenantIdeitherA guest of your workspace, settled with instead of a customer record. See below.
invoiceTemplateIdyesMust belong to you
issueDate / dueDatenoDefaults: today / +14 days
itemsyesAt least one
items[].unityesPiece, Hour, Kilogram, Liter, Meter, … (enum name, case-sensitive)
items[].supplyKindnoService (default) or Goods. With a template that has autoTaxCase, the VAT case is worked out with the roles of a self-billed credit note: the recipient is the seller, you are the buyer.

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",
  "recipientTenantId": null,
  "recipientName": "Subunternehmer Novak e.U."
}

Settling with a guest instead of a customer

Send recipientTenantId in place of customerId to make the document out to somebody who booked hours in your workspace. They have no customer record and never get one: their name, address and bank details are read from their own business data at render time, so the payout account is always the one they maintain.

{
  "recipientTenantId": 17,
  "invoiceTemplateId": 1,
  "issueDate": "2026-08-21T00:00:00Z",
  "dueDate": "2026-09-04T00:00:00Z",
  "introductionText": "Abrechnung August 2026",
  "items": [
    { "itemKey": "001", "description": "Umbau Nord — Verkabelung", "quantity": 9.5, "unit": "Hour", "pricePerUnit": 65.00, "sortOrder": 1 }
  ]
}

Refused with 400 invalid_state when the recipient is not a guest of yours, has not accepted the invitation, a plan on either side lapsed, or their business data is incomplete — the message names what is missing. A credit note is finalized on creation and cannot be corrected afterwards, so it is refused before it is written rather than issued with a placeholder company name or an empty IBAN.

customerId is nullable on the way out. It is null exactly on the documents settled with a guest, where recipientTenantId is set instead. Every document created without that field still reports a number, so nothing changes for an integrator that does not use the feature — but deserialising customerId as a required int will break the moment one of these arrives. recipientName is filled either way and is the same name printed on the archived PDF.

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.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"
}

customerId is nullable here too — see the note under POST /api/gutschriften.

Purchase Orders

3 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 yours
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 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).

POST /api/purchase-orders/preview Preview a purchase order without creating it

Same body as POST /api/purchase-orders, same validation, same errors — but the response is the rendered PDF and nothing is stored: no purchase order row, no consumed number, nothing archived. finalize is ignored here.

The number on the page reads PO-2026-07-PREVIEW, because the number circle stays untouched; everything else matches the document the real call produces.

curl -X POST https://timelane.cloud/api/purchase-orders/preview \
  -H "X-Api-Key: qsp_live_…" \
  -H "Content-Type: application/json" \
  -d '{"supplierId":42,"invoiceTemplateId":1,"deliveryAddress":"Lager Süd, Industriestraße 42, 1230 Wien","items":[{"description":"Managed Switch 24-Port","quantity":2,"pricePerUnit":380.00}]}' \
  -o preview.pdf

Responses

  • 200 OK → application/pdf
  • 404 not_found → supplier/template not yours
  • 422 business_info_missing → configure your business details first
  • 400 invalid_body / invalid_argument / invalid_state
  • 403 plan_forbidden
Ein unbehandelter Fehler ist aufgetreten. Neu laden 🗙