Kunden & Artikel
Kundenstamm und den wiederverwendbaren Positionskatalog mit Kundenpreisen verwalten.
Manage customers and the reusable line-item catalogue (positions).
Operationen
-
GET/customersList customers (master data), ordered by customer number (paginated) -
POST/customersCreate a customer (customerNumber is assigned by the server) -
GET/customers/{id}Get a customer by id -
PUT/customers/{id}Update a customer (full replace of editable fields) -
DELETE/customers/{id}Delete a customer (linked invoices keep their snapshot) -
GET/customers/{id}/positionsThis customer's COMPLETE set of position assignments (Zuordnungen) -
PUT/customers/{id}/positionsReplace this customer's COMPLETE set of position assignments (Zuordnungen) -
GET/positionsList the position catalog (Positionsvorlagen), ordered by description (paginated) -
POST/positionsCreate a position -
GET/positions/{id}Get a position by id (with its customer assignments) -
PUT/positions/{id}Update a position (full replace of editable fields) -
DELETE/positions/{id}Delete a position (ALWAYS succeeds — never 409) -
GET/customers/{id}/open-itemsPer-customer open-items statement (Kontoauszug / Offene Posten)
List customers (master data), ordered by customer number (paginated)
GET /customers
- Erforderlicher Scope
-
customers:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
page | Query | integer | Page number (1-indexed). Standard: 1 Bereich: ≥ 1 |
pageSize | Query | integer | Number of items per page. Standard: 25 Bereich: ≥ 1, ≤ 100 |
q | Query | string | Free-text filter, case-insensitive substring match over a CLOSED per-resource field set (invoices: invoice number + buyer name; offers: offer number + buyer name; customers: name + email). Trimmed; an empty/whitespace-only value is treated as absent, not as an error. Results are bounded by the pagination limit and by your own entity's scope. Deliberately NOT a fuzzy/ranked search — it is a plain substring match, and it does not search line items, notes or amounts. The term is never written to logs, audit records or webhook payloads. Länge: max. 100 |
Antworten
-
200Paginated list of customers
application/jsonobject -
400The query string was refused. Three distinguishable facts share this status, and a client should branch on the
errorcode rather than on the status:unknown_query_parameter— the request named one or more query parameters this deployment does not define, andparameterslists their names. Nothing was filtered and nothing was written. This is a DEPLOY-ORDER signal: it is what a client released ahead of the API sees, and it replaces the older behaviour in which an unrecognised parameter was silently ignored and the request answered200 OKwith the UNFILTERED collection — indistinguishable, to the caller, from the filter having been applied.validation_error— a parameter this endpoint does define carried an unusable value (including a required one being absent or too short).issues[]names the parameter and what was wrong with it.invalid_cursor— on cursor-paginated endpoints, the pagination cursor is malformed. No response on this path ever echoes a submitted VALUE; only parameter names appear.application/jsonUnknownQueryParameterError | ValidationError
Create a customer (customerNumber is assigned by the server)
POST /customers
Honors the optional Idempotency-Key header so a retried creation is a replay, not a duplicate.
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
Idempotency-Key | Header | string | Optional client-supplied key that makes this mutating request safe to retry. The FIRST request runs and its outcome is recorded keyed by Länge: max. 255 |
Request-Body erforderlich
application/json CreateCustomer
Antworten
-
201The created customer (or, on an Idempotency-Key replay, the recorded response)
application/jsonCustomer -
400Validation error
application/jsonValidationError -
409Idempotency-Key conflict.
IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY— the key was already used with a different request body;REQUEST_IN_PROGRESS— the first request bearing this key is still in flight (retryable).application/jsonobject
Get a customer by id
GET /customers/{id}
- Erforderlicher Scope
-
customers:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Antworten
-
200The customer
application/jsonCustomer -
404Not found
Update a customer (full replace of editable fields)
PUT /customers/{id}
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Request-Body erforderlich
application/json ReplaceCustomer
Antworten
-
200The updated customer
application/jsonCustomer -
400Validation error
application/jsonValidationError -
404Not found
Delete a customer (linked invoices keep their snapshot)
DELETE /customers/{id}
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Antworten
-
204Deleted
-
404Not found
This customer's COMPLETE set of position assignments (Zuordnungen)
GET /customers/{id}/positions
The read counterpart of the whole-set PUT below, and the ONLY sanctioned source for that PUT's body.
COMPLETE BY CONTRACT. This response is never paginated and never truncated: there is no page/pageSize/cursor/limit parameter, and assignments.length IS the total. Should the stored set ever exceed the SAME ceiling the sibling PUT enforces on its body (ReplaceCustomerPositions.assignments.maxItems — one constant in code, not two numbers), this endpoint returns 409 assignment_set_too_large rather than a partial body. A client that checks nothing still cannot obtain a truncated set.
Why it exists (and see the PUT, whose earlier reasoning this supersedes): deriving the assignment set by filtering a page of GET /positions is unsafe for ANY page size, because the PUT is a whole-set replace and an unloaded assignment reads as a deletion.
Each entry embeds the position fields the Zuordnungen table renders (Bezeichnung, Katalogpreis, Einheit, Waehrung) so the screen needs no second lookup — but deliberately NOT the position's OTHER customers (Position.assignments): this is a read about ONE customer and must not return every other customer's name and price.
Ordered by the position's description (the catalog's own order), with positionId as the deterministic tiebreak. Entity- and mode-scoped.
- Erforderlicher Scope
-
customers:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Antworten
-
200The complete assignment set (an empty array when none).
application/jsonCustomerPositionAssignments -
403Caller lacks the
customers:readscope. -
404Customer not found. A customer with NO assignments is
200with an empty array — never404. -
409assignment_set_too_large— the stored set exceeds the contract ceiling, so a COMPLETE response cannot be returned and a partial one is refused. Unreachable through this API (the PUT caps what can be written); it exists so the completeness guarantee is enforced rather than assumed.
Replace this customer's COMPLETE set of position assignments (Zuordnungen)
PUT /customers/{id}/positions
Whole-set replace: the body IS the resulting assignment set. Any assignment not present in assignments is REMOVED. An empty array is valid and clears all of this customer's assignments.
PUT (not PATCH/POST) because the request describes the complete desired state of a set, which is exactly PUT's semantics. This shape is chosen deliberately over per-row create/update/delete calls: the customer edit page is a single table with a single "Zuordnungen speichern" button, so one atomic call means one validation path and no partial-failure states to design a UI for. The write runs in a transaction — it either applies completely or not at all.
It is scoped under the customer, not the position, because the assignment (and its price override, the "Kundenpreis") is a fact ABOUT THE CUSTOMER and is always read per customer.
SUPERSEDED 2026-08-09. This description used to state that there was deliberately NO GET counterpart, because "the customer edit page must load the full catalog anyway, so a dedicated GET would be a second shape for data the client necessarily already holds."
THE PREMISE WAS FALSE, AND ITS FALSENESS WAS THIS DEFECT. GET /positions is capped at pageSize 100 — a HARD maximum in the shared pagination schema, not a call-site choice — while this write accepts 500 assignments. So for any catalog above 100 entries the client did NOT hold the data: it silently omitted the unloaded assignments from this whole-set body, and this endpoint deleted them. No error, no warning, and the record read as if the user had removed them.
The read counterpart is GET /customers/{id}/positions, and it is COMPLETE BY CONTRACT — never paginated, never truncated, 409 rather than a partial body. Build this PUT's body from that read. Never from a page of GET /positions, at any page size.
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Request-Body erforderlich
application/json ReplaceCustomerPositions
Antworten
-
200The saved assignment set
application/jsonobject -
400Validation error, or
invalid_positionwhen a positionId is unknown.application/jsonValidationError -
404Customer not found
List the position catalog (Positionsvorlagen), ordered by description (paginated)
GET /positions
Returns one PAGE of the catalog, each position carrying its customer assignments embedded (assignments). This is deliberate and is the endpoint's main design constraint: the invoice form must be able to resolve ANY customer's assigned positions and their effective unit price with ZERO extra network requests when the user changes the customer (the customer is chosen client-side inside an already-rendered form).
Effective price for a customer = assignments[].unitPriceNetOverride when that customer has an assignment with a non-null override, else the position's own unitPriceNet (the Katalogpreis).
assignments.length is also the "Kunden" count on the catalog list page — no separate aggregate endpoint is needed.
Assignments are scoped to the positions on the current page only. The underlying implementation uses 2 constant queries per request. Sized for the target scale (tens of positions).
- Erforderlicher Scope
-
customers:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
page | Query | integer | Page number (1-indexed). Standard: 1 Bereich: ≥ 1 |
pageSize | Query | integer | Number of items per page. Standard: 25 Bereich: ≥ 1, ≤ 100 |
Antworten
-
200Paginated catalog, sorted by description, assignments embedded
application/jsonobject -
400The query string was refused. Three distinguishable facts share this status, and a client should branch on the
errorcode rather than on the status:unknown_query_parameter— the request named one or more query parameters this deployment does not define, andparameterslists their names. Nothing was filtered and nothing was written. This is a DEPLOY-ORDER signal: it is what a client released ahead of the API sees, and it replaces the older behaviour in which an unrecognised parameter was silently ignored and the request answered200 OKwith the UNFILTERED collection — indistinguishable, to the caller, from the filter having been applied.validation_error— a parameter this endpoint does define carried an unusable value (including a required one being absent or too short).issues[]names the parameter and what was wrong with it.invalid_cursor— on cursor-paginated endpoints, the pagination cursor is malformed. No response on this path ever echoes a submitted VALUE; only parameter names appear.application/jsonUnknownQueryParameterError | ValidationError
Create a position
POST /positions
Honors the optional Idempotency-Key header so a retried creation is a replay, not a duplicate.
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
Idempotency-Key | Header | string | Optional client-supplied key that makes this mutating request safe to retry. The FIRST request runs and its outcome is recorded keyed by Länge: max. 255 |
Request-Body erforderlich
application/json CreatePosition
Antworten
-
201The created position (with an empty
assignmentsarray; or, on an Idempotency-Key replay, the recorded response)application/jsonPosition -
400Validation error
application/jsonValidationError -
409Idempotency-Key conflict.
IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY— the key was already used with a different request body;REQUEST_IN_PROGRESS— the first request bearing this key is still in flight (retryable).application/jsonobject
Get a position by id (with its customer assignments)
GET /positions/{id}
- Erforderlicher Scope
-
customers:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Antworten
-
200The position
application/jsonPosition -
404Not found
Update a position (full replace of editable fields)
PUT /positions/{id}
Does NOT affect any existing invoice or subscription template. Those hold SNAPSHOTS of the values they were created with, never a reference to this row. Assignments are not touched here — replace them via PUT /customers/{id}/positions.
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Request-Body erforderlich
application/json ReplacePosition
Antworten
-
200The updated position
application/jsonPosition -
400Validation error
-
404Not found
Delete a position (ALWAYS succeeds — never 409)
DELETE /positions/{id}
There is NO "in use" check and no 409 by design. Existing invoices and subscription templates snapshot their line items, so no document can reference this row and nothing can be broken by removing it. Only the customer assignments (and their price overrides) cascade away.
- Erforderlicher Scope
-
customers:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) |
Antworten
-
204Deleted
-
404Not found
Per-customer open-items statement (Kontoauszug / Offene Posten)
GET /customers/{id}/open-items
For one customer, every OPEN invoice — status='issued' AND payment_state <> 'paid', INCLUDING not-yet-due invoices (the all-open sibling of GET /dunning/overdue, which returns only the overdue subset). Per row, server-computed (never re-derived in web): effective due date, openAmountMinor (= totalGross − amountPaid − the same-currency partial credit notes), a due/overdue flag + daysOverdue, the current Mahnstufe, and the dunning nextAction descriptor — plus customer-level rollup totals (open + overdue-only subtotal). Bounded (a single customer's open set is small), not cursor-paginated. Mode-scoped. DEGRADES GRACEFULLY where dunning is enabledis off:dunningEnabled=falseand thenextAction/currentLevel*fields arenull`, while open amounts + due/overdue are still returned (never a 404 on the flag).
- Erforderlicher Scope
-
documents:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The customer whose open items are returned. |
Antworten
-
200The customer's open-items statement (all fields server-derived).
application/jsonCustomerOpenItems -
400idis not a UUID. -
403Caller lacks the
documents:readscope.
Datenmodelle
Jedes Datenmodell dieser Gruppe einmal, mit allen Feldern – auch den verschachtelten. Verschachtelte Objekte sind aufklappbar; ein benannter Typ verweist auf seinen eigenen Abschnitt.
Customer object
Customer master data (invoicing essentials only; GDPR-minimized).
| Feld | Typ | Beschreibung | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | string (uuid) | |||||||||||||
customerNumber | string | Auto-assigned Kundennummer, e.g. "K-00001". Counted per company and per mode: each company's live customers and its sandbox customers each run their own series from K-00001, so the same number can belong to a different customer in another company or in the other mode. Unique within one company and mode; gaps are possible (a deleted customer's number is not reused). | ||||||||||||
name | string | |||||||||||||
addressLine1 | string | null | |||||||||||||
postalCode | string | null | |||||||||||||
city | string | null | |||||||||||||
countryCode | string | null | ISO 3166-1 alpha-2 | ||||||||||||
vatId | string | null | USt-IdNr | ||||||||||||
email | string (email) | null | Recipient email for invoice delivery | ||||||||||||
defaultBuyerReference | string | null | |||||||||||||
defaultElectronicAddress | string | null | EN 16931 BT-49 default for this customer — a Leitweg-ID or an email, prefilled onto new invoices and subscriptions. Bare value only: the scheme ( Länge: max. 200 | ||||||||||||
documentTarget | string | The customer's document-target intent (B2B / B2G), snapshotted onto every new invoice/subscription for this customer. Always present (defaults to 'b2b'). Werte: "b2b", "b2g" | ||||||||||||
ccEmails | Array<string (email)> | null | Standing copy recipients („Kopie an"): every automatic invoice e-mail to this customer is also sent to these addresses, visibly in the Elemente: max. 5 | ||||||||||||
bccEmails | Array<string (email)> | null | Standing blind copy recipients („Blindkopie an"): every automatic invoice e-mail to this customer is also delivered to these addresses without naming them in any header. Elemente: max. 5 | ||||||||||||
capabilities | CustomerCapabilities | |||||||||||||
restrictedDocumentCount | integer | DERIVED rollup — count of this customer's documents currently DSGVO Art. 18 processing-restricted. Present only on the single-customer detail read ( | ||||||||||||
suppressedAddress | object | null | Set when this customer's e-mail address is on the account's suppression list, because earlier mail to it was permanently refused by the receiving mail system. While it is set, no further mail is sent to that address; the remedy is to record a different address on this customer. Present on the single-customer detail read only — an address is not a list-scan fact and does not belong in a table. Felder
| ||||||||||||
createdAt | string (date-time) | |||||||||||||
updatedAt | string (date-time) | |||||||||||||
mode | Mode |
Verwendet von:
GET /customers , POST /customers , GET /customers/{id} , PUT /customers/{id}
PaginationMeta object
Pagination metadata returned alongside every paginated list.
| Feld | Typ | Beschreibung |
|---|---|---|
total erforderlich | integer | Total number of records matching the current filter. |
page erforderlich | integer | Current page (1-indexed, echoes the request). |
pageSize erforderlich | integer | Records per page (echoes the request). |
totalPages erforderlich | integer | Total number of pages. Minimum 1, even when total is 0. |
Verwendet von:
GET /customers , GET /positions
CustomerCapabilities object
What this deployment offers on the customer record. Present on the single-customer read and on the list envelope.
| Feld | Typ | Beschreibung |
|---|---|---|
copyRecipients erforderlich | boolean | Whether |
Verwendet von:
GET /customers
UnknownQueryParameterError object
The request named query parameters this endpoint does not define. A list endpoint refuses them rather than ignoring them, so a caller can never mistake an UNFILTERED collection for a filtered one.
| Feld | Typ | Beschreibung |
|---|---|---|
error erforderlich | string | Werte: "unknown_query_parameter" |
parameters erforderlich | Array<string> | The unrecognised parameter NAMES, deduplicated and sorted. Bounded at ten entries, and a name longer than 64 characters is omitted — a parameter name is caller-supplied text, and this response never reflects unbounded caller input. The array may therefore be EMPTY while the refusal still stands: the refusal is decided by the request, not by the length of this list. |
message | string | |
requestId | string |
Verwendet von:
GET /customers , GET /positions
ValidationError object
A refused request body. error is validation_error when the body was read and failed validation (each problem is listed in issues), or invalid_json when the body is not valid JSON at all (then issues is absent). An unparsable body is answered 400 invalid_json on every operation that takes a JSON body. The submitted body is never echoed.
| Feld | Typ | Beschreibung | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
error | string | |||||||||||||
issues | Array<object> | Felder
|
Verwendet von:
GET /customers , POST /customers , PUT /customers/{id} , PUT /customers/{id}/positions , GET /positions , POST /positions
CreateCustomer object
Create payload. customerNumber is NOT accepted — the server assigns it. On CREATE only, omitting the public-sector/business target records an ordinary business customer; on the replace it is REQUIRED (see ReplaceCustomer).
| Feld | Typ | Beschreibung |
|---|---|---|
name erforderlich | string | |
addressLine1 | string | |
postalCode | string | |
city | string | |
countryCode | string | ISO 3166-1 alpha-2 (2 letters) |
vatId | string | |
email | string (email) | Recipient email for invoice delivery |
defaultBuyerReference | string | |
defaultElectronicAddress | string | EN 16931 BT-49 default — a Leitweg-ID or an email, prefilled onto new invoices and subscriptions. Send the bare value; the scheme ( Länge: max. 200 |
documentTarget | string | Whether this customer is a public authority (b2g) or an ordinary business (b2b). It decides whether their invoices are later produced in the format public authorities require. On a CREATE, omitting it records an ordinary business customer. On the REPLACE it is REQUIRED — see ReplaceCustomer. The same property is KEPT rather than reset on the invoice and recurring-template replaces, which is why it is described here per property. Werte: "b2b", "b2g" |
ccEmails | Array<string (email)> | null | Standing copy recipients („Kopie an") for every automatic invoice e-mail to this customer. Each entry is one plain address with no display name; the domain is lower-cased and the local part kept as sent. An address equal to Elemente: max. 5 |
bccEmails | Array<string (email)> | null | Standing blind copy recipients („Blindkopie an"), under the same rules as Elemente: max. 5 |
Verwendet von:
POST /customers
ReplaceCustomer object
Replace payload for a customer. Identical to the create payload with one difference: the public-sector/business target is REQUIRED. This body describes the whole customer, so an omitted property states that the property has no value — and a customer is always one or the other, so there is nothing for an omission to mean. Until this became required, omitting it stored 'b2b', which silently converted a public authority into an ordinary business. The same whole-object rule applies to ccEmails and bccEmails: omitting a list clears the stored copy recipients, so a client must send back the lists it read.
| Feld | Typ | Beschreibung |
|---|---|---|
name erforderlich | string | |
addressLine1 | string | |
postalCode | string | |
city | string | |
countryCode | string | ISO 3166-1 alpha-2 (2 letters) |
vatId | string | |
email | string (email) | Recipient email for invoice delivery |
defaultBuyerReference | string | |
defaultElectronicAddress | string | EN 16931 BT-49 default — a Leitweg-ID or an email, prefilled onto new invoices and subscriptions. Send the bare value; the scheme ( Länge: max. 200 |
documentTarget erforderlich | string | Whether this customer is a public authority (b2g) or an ordinary business (b2b). It decides whether their invoices are later produced in the format public authorities require. On a CREATE, omitting it records an ordinary business customer. On the REPLACE it is REQUIRED — see ReplaceCustomer. The same property is KEPT rather than reset on the invoice and recurring-template replaces, which is why it is described here per property. Werte: "b2b", "b2g" |
ccEmails | Array<string (email)> | null | Standing copy recipients („Kopie an") for every automatic invoice e-mail to this customer. Each entry is one plain address with no display name; the domain is lower-cased and the local part kept as sent. An address equal to Elemente: max. 5 |
bccEmails | Array<string (email)> | null | Standing blind copy recipients („Blindkopie an"), under the same rules as Elemente: max. 5 |
Verwendet von:
PUT /customers/{id}
CustomerPositionAssignments object
One customer's COMPLETE set of position assignments (Zuordnungen). NOT paginated: assignments.length is the total. See GET /customers/{id}/positions for the completeness contract.
| Feld | Typ | Beschreibung |
|---|---|---|
customerId erforderlich | string (uuid) | |
assignments erforderlich | Array<CustomerPositionAssignmentDetail> |
Verwendet von:
GET /customers/{id}/positions
ReplaceCustomerPositions object
The COMPLETE desired assignment set for one customer. Anything absent is removed. An empty array clears all assignments. Duplicate positionId values are rejected (the resulting override would be ambiguous).
| Feld | Typ | Beschreibung | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
assignments erforderlich | Array<object> | Elemente: max. 500 Felder
|
Verwendet von:
PUT /customers/{id}/positions
CustomerPositionAssignment object
One saved assignment, as returned by PUT /customers/{id}/positions.
| Feld | Typ | Beschreibung |
|---|---|---|
positionId erforderlich | string (uuid) | |
unitPriceNetOverride erforderlich | DecimalString | null | null = no override = use the catalog price. |
Verwendet von:
PUT /customers/{id}/positions
Position object
One catalog entry ("Positionsvorlage") — a reusable invoice line item. SNAPSHOT SEMANTICS: inserting a position onto an invoice or subscription COPIES its values into the line item. No invoice line ever holds a reference back to a position, and the invoice create/update payload is completely unchanged by this feature. Editing or deleting a position therefore cannot alter any existing document.
| Feld | Typ | Beschreibung |
|---|---|---|
id erforderlich | string (uuid) | |
description erforderlich | string | Bezeichnung. Copied verbatim into the line item's description. |
unitCode erforderlich | string | EN 16931 BT-130 — the unit of measure this entry carries onto an invoice line, from the UN/ECE Recommendation 20 code list with the Recommendation 21 extension (rule BR-CL-23). E.g. C62 (piece), HUR (hour), KGM. NO PATTERN IS PUBLISHED ON THIS READ: catalogue entries saved before this input boundary existed may carry a value outside the code list, and they are returned exactly as they were saved. |
unitPriceNet erforderlich | DecimalString | Katalogpreis — the fallback unit price. NUMERIC(15,4), as on the line item. |
taxRatePercent erforderlich | DecimalString | |
currency erforderlich | string | ISO 4217. The client filters the picker to positions matching the invoice's currency; the api only stores and returns it. |
defaultQuantity erforderlich | DecimalString | null | Standardmenge, prefilled as the line's quantity on insert. null = NOT SET — the client falls back to "1". |
assignments erforderlich | Array<PositionAssignment> | Customers this position is assigned to. Always present (empty array when unassigned), on both the list and the detail endpoint. |
createdAt erforderlich | string (date-time) | |
updatedAt erforderlich | string (date-time) | |
mode | Mode |
Verwendet von:
GET /positions , POST /positions , GET /positions/{id} , PUT /positions/{id}
CreatePosition object
Create payload. Assignments are NOT part of this body — they are replaced per customer via PUT /customers/{id}/positions. On CREATE only, omitting the currency records EUR; on the replace it is REQUIRED (see ReplacePosition).
| Feld | Typ | Beschreibung |
|---|---|---|
description erforderlich | string | Länge: min. 1, max. 500 |
unitCode erforderlich | string | EN 16931 BT-130 — the unit of measure this catalogue entry carries onto an invoice line. This is a CODED element: the value must come from the UN/ECE Recommendation 20 code list with the Recommendation 21 extension, as enforced by rule BR-CL-23, and NOT from your own vocabulary. Codes are UPPERCASE and are never corrected for you — "hur" is not "HUR". Common values: C62 (piece), HUR (hour), DAY, KGM (kilogram), MTR (metre), LTR (litre), MTK (square metre), KWH. The full accepted set is much wider than these examples; anything matching the published pattern and present in the code list is accepted. ANNOUNCED NARROWING: this endpoint currently ACCEPTS values outside the code list and records them, so an existing integration keeps working. Refusal is a separate, later change. Send a listed code now; the pattern above is what will be enforced. Länge: min. 2, max. 3 Muster: ^[A-Z0-9]{2,3}$ |
unitPriceNet erforderlich | MoneyString | |
taxRatePercent erforderlich | PercentString | |
currency | string | ISO 4217. This price is copied onto invoice lines, so the currency is copied with it. On a CREATE, omitting it records EUR. On the REPLACE it is REQUIRED — see ReplacePosition. Until it became required, omitting it on a replace silently repriced a foreign-currency catalogue entry in euros. Länge: min. 3, max. 3 |
defaultQuantity | DecimalString | null | Standardmenge. Omit or send null for "not set" (client falls back to "1"). |
Verwendet von:
POST /positions
ReplacePosition object
Replace payload for a catalogue entry. Identical to the create payload with one difference: the currency is REQUIRED. This body describes the whole entry, so an omitted property states that the property has no value — and a price always has a currency, so there is nothing for an omission to mean. Until this became required, omitting it stored EUR, which silently repriced a foreign-currency entry.
| Feld | Typ | Beschreibung |
|---|---|---|
description erforderlich | string | Länge: min. 1, max. 500 |
unitCode erforderlich | string | EN 16931 BT-130 — the unit of measure this catalogue entry carries onto an invoice line. This is a CODED element: the value must come from the UN/ECE Recommendation 20 code list with the Recommendation 21 extension, as enforced by rule BR-CL-23, and NOT from your own vocabulary. Codes are UPPERCASE and are never corrected for you — "hur" is not "HUR". Common values: C62 (piece), HUR (hour), DAY, KGM (kilogram), MTR (metre), LTR (litre), MTK (square metre), KWH. The full accepted set is much wider than these examples; anything matching the published pattern and present in the code list is accepted. ANNOUNCED NARROWING: this endpoint currently ACCEPTS values outside the code list and records them, so an existing integration keeps working. Refusal is a separate, later change. Send a listed code now; the pattern above is what will be enforced. Länge: min. 2, max. 3 Muster: ^[A-Z0-9]{2,3}$ |
unitPriceNet erforderlich | MoneyString | |
taxRatePercent erforderlich | PercentString | |
currency erforderlich | string | ISO 4217. This price is copied onto invoice lines, so the currency is copied with it. On a CREATE, omitting it records EUR. On the REPLACE it is REQUIRED — see ReplacePosition. Until it became required, omitting it on a replace silently repriced a foreign-currency catalogue entry in euros. Länge: min. 3, max. 3 |
defaultQuantity | DecimalString | null | Standardmenge. Omit or send null for "not set" (client falls back to "1"). |
Verwendet von:
PUT /positions/{id}
CustomerOpenItems object
The per-customer open-items statement — every open invoice for one customer + the rollup totals. All fields server-derived.
| Feld | Typ | Beschreibung |
|---|---|---|
customerId erforderlich | string (uuid) | |
dunningEnabled erforderlich | boolean | Whether the dunning columns ( |
items erforderlich | Array<OpenItem> | |
totals erforderlich | OpenItemsTotals |
Verwendet von:
GET /customers/{id}/open-items
Mode string
Mode echo ("unmissable live/test indicator"): the row's own live/test mode, echoed on every scoped-resource representation so a live vs. test record is unmissable. Read from the record's mode column (never re-derived from the request credential); live for legacy rows created before mode existed.
- Werte: "live", "test"
Beispiel: "live"
Verwendet von:
GET /customers , POST /customers , GET /customers/{id} , PUT /customers/{id} , GET /positions , POST /positions , GET /positions/{id} , PUT /positions/{id}
CustomerPositionAssignmentDetail object
One assignment, with the assigned position's own fields embedded so the Zuordnungen table renders without a second lookup.
The embedded field set is exactly the fields an insert COPIES onto an invoice line item (the snapshot semantics of the catalogue copy). It is deliberately NOT the full Position: assignments (this position's OTHER customers) is omitted as a data-minimisation rule — this is a read about one customer — and createdAt/updatedAt are omitted because beside an assignment they read as the assignment's timestamps, which they are not.
| Feld | Typ | Beschreibung |
|---|---|---|
positionId erforderlich | string (uuid) | |
description erforderlich | string | The position's Bezeichnung. |
unitCode erforderlich | string | EN 16931 BT-130 — the unit of measure this entry carries onto an invoice line, from the UN/ECE Recommendation 20 code list with the Recommendation 21 extension (rule BR-CL-23). E.g. C62 (piece), HUR (hour), KGM. NO PATTERN IS PUBLISHED ON THIS READ: catalogue entries saved before this input boundary existed may carry a value outside the code list, and they are returned exactly as they were saved. |
unitPriceNet erforderlich | DecimalString | Katalogpreis — the position's list price. |
taxRatePercent erforderlich | DecimalString | |
currency erforderlich | string | ISO 4217, trimmed (the column is CHAR(3), blank-padded). |
defaultQuantity erforderlich | DecimalString | null | Standardmenge. null = NOT SET — the client falls back to "1". |
unitPriceNetOverride erforderlich | DecimalString | null | Kundenpreis. null = NO override = bill this customer |
Verwendet von:
GET /customers/{id}/positions
DecimalString string
A fixed-precision decimal as a string, e.g. "9542.50". Read shapes deliberately publish no scale: a document issued before the request scales below were introduced may carry more decimals than a new request may send, and such a document is served from the archive rather than re-created.
Beispiel: "9542.50"
Verwendet von:
GET /customers/{id}/positions , PUT /customers/{id}/positions , GET /positions , POST /positions , GET /positions/{id} , PUT /positions/{id}
PositionAssignment object
A customer this position is assigned to (Zuordnung), with that customer's optional price override (Kundenpreis). The customer's number and name are joined in at READ time (never stored on the assignment, always fresh) so the catalog list, the position edit page and the customer edit page are each served without a second request.
| Feld | Typ | Beschreibung |
|---|---|---|
customerId erforderlich | string (uuid) | |
customerNumber erforderlich | string | Kundennummer, e.g. "K-00001" — counted per company and mode, unique only within them. |
customerName erforderlich | string | |
unitPriceNetOverride erforderlich | DecimalString | null | Kundenpreis. null = NO override = bill this customer the catalog price ( |
Verwendet von:
GET /positions , POST /positions , GET /positions/{id} , PUT /positions/{id}
MoneyString string
A monetary amount as a string, with at most two decimal places — the scale amounts are billed in. A finer value is refused rather than rounded: rounding it would change the number the caller sent, and because a unit price is rounded to whole cents before being multiplied by the quantity, that change is then multiplied too. A price of "12.999" at quantity 1000 billed 13000.00 against an honest 12999.00; "0.005" at the same quantity billed double.
- Muster: ^\d+(\.\d{1,2})?$
Beispiel: "9542.50"
Verwendet von:
POST /positions , PUT /positions/{id}
PercentString string
A VAT rate as a string, with at most one decimal place — the scale the rendered document can carry. One decimal covers every German rate in use: the standard rates and the agricultural and forestry averages. A finer rate is refused rather than printed, because the document would then state a rate that does not produce the tax amount printed beside it.
- Muster: ^\d+(\.\d{1})?$
Beispiel: "19"
Verwendet von:
POST /positions , PUT /positions/{id}
OpenItem object
One open invoice on a customer's statement — status='issued' AND payment_state <> 'paid', due OR not-yet-due. Every field server-derived.
| Feld | Typ | Beschreibung |
|---|---|---|
invoiceId erforderlich | string (uuid) | |
invoiceNumber erforderlich | string | null | |
customerId erforderlich | string (uuid) | null | |
buyerName erforderlich | string | |
currency erforderlich | string | |
effectiveDueDate erforderlich | string (date) | COALESCE(due_date, issue_date + 30). |
daysOverdue erforderlich | integer | CURRENT_DATE − effectiveDueDate (whole days; NEGATIVE when not yet due). |
overdue erforderlich | boolean | True iff past the effective due date (daysOverdue > 0). |
openAmountMinor erforderlich | integer | What is still owed, in minor units: totalGross − amountPaid − the same-currency partial credit notes. |
totalGrossMinor erforderlich | integer | The invoice gross in minor units. |
currentLevelLabel erforderlich | string | null | The derived current Mahnstufe label; null when none sent OR dunning is off. |
currentLevelSentAt erforderlich | string (date-time) | null | When the current level was sent; null when none OR dunning is off. |
nextAction erforderlich | DunningNextAction | null | The server-computed next-action descriptor; null when dunning is not enabled. Varianten
|
Verwendet von:
GET /customers/{id}/open-items
OpenItemsTotals object
Customer-level rollup (all minor units).
| Feld | Typ | Beschreibung |
|---|---|---|
openAmountMinor erforderlich | integer | Σ openAmountMinor over every open invoice. |
openCount erforderlich | integer | Count of open invoices. |
overdueAmountMinor erforderlich | integer | Σ openAmountMinor over the overdue subset only. |
overdueCount erforderlich | integer | Count of open invoices past their effective due date. |
Verwendet von:
GET /customers/{id}/open-items
DunningNextAction object
The SERVER-COMPUTED next-action descriptor for one overdue invoice. web renders it verbatim — the Mahnstufe schedule stays server-side. Produced by the shared send-precondition helper so a future send path cannot drift from what the row hint shows.
| Feld | Typ | Beschreibung |
|---|---|---|
nextLevelKey erforderlich | string | null | The next level's key, or null when all reached levels are sent. |
nextLevelLabel erforderlich | string | null | The next level's label (the dynamic kebab item label), or null. |
sendable erforderlich | boolean | Whether the next level can be sent right now. |
blockedReason erforderlich | string | null | Why the next level is not sendable, or null when sendable is true. Werte: "cancelled_by_correction", "correction_chain_indeterminate", "not_sent", "no_recipient", "no_level_due", "all_sent", "processing_restricted" |
nextLevelAvailableFrom erforderlich | string (date) | null | ISO date the next level becomes due, when it is not yet due (no_level_due), else null. |
Verwendet von:
GET /customers/{id}/open-items
Nach oben · Alle Ressourcengruppen · OpenAPI-Spezifikation (YAML)