Zahlungsabgleich
Zahlungseingänge empfangen und automatisch den passenden Rechnungen zuordnen.
Inbound payment reconciliation: sources, received events, and matching.
Operationen
-
GET/inbound-sources/summaryHow many inbound sources are configured (counts only) -
GET/inbound-sourcesList inbound sources for the active tenant/entity -
POST/inbound-sourcesRegister an inbound source -
GET/inbound-sources/{id}Get one inbound source (masked config) -
PATCH/inbound-sources/{id}Update an inbound source (subscribedTypes / status) -
DELETE/inbound-sources/{id}Disable an inbound source (soft-delete) -
POST/inbound-sources/{id}/reveal-configReveal the full verification config (audit-logged, repeatable) -
POST/inbound-sources/{id}/rotate-secretRotate the verification secret (signed source-types only) -
PUT/inbound-sources/{id}/auto-reconcileSwitch automatic booking on or off for ONE payment source -
POST/inbound-sources/{id}/simulateTestanfrage simulieren — sandbox-locked synthetic received-event -
POST/inbound/{slug}Public signed inbound-event receiver (the security spine) -
GET/inbound-eventsList received inbound events (cursor-paginated, metadata-only) -
GET/inbound-events/{id}One inbound event + its processing-step timeline (metadata-only) -
POST/inbound-events/{id}/reprocessRe-dispatch (replay) a logged inbound event -
POST/inbound-events/{id}/confirmHuman-confirm the suggested payment match (Zahlungsabgleich) -
POST/invoices/{id}/paymentsRecord a payment against an invoice -
GET/invoices/{id}/paymentsThe per-invoice payment ledger -
POST/invoices/{id}/payment-allocations/{allocationId}/reversalReverse a recorded payment allocation
How many inbound sources are configured (counts only)
GET /inbound-sources/summary
Readable by readonly and above, unlike the rest of this resource, which is admin-only. It exists so the invoicing application can tell „no source has ever been set up" apart from „a source exists and nothing has arrived yet" — two different sentences with two different next actions — for the accounting staff who read the payment inbox and are not administrators. The listing cannot serve them: it projects the receiving address, and registering a source stays a deliberate administrative act. ⛔ The answer is two integers and will stay two integers. No identifier, no address, no kind — a row here would be the listing with a weaker gate.
- Erforderlicher Scope
-
payments:read - Zugang
- angemeldete Sitzung oder API-Schlüssel
(
any)
Antworten
-
200Counts only.
application/jsonobject -
403An OIDC caller who is not an active member (
insufficient_role), or a key withoutpayments:read(insufficient_scope).
List inbound sources for the active tenant/entity
GET /inbound-sources
Admin/owner only (requireTenantContext + requireRole('admin')). Secrets are never returned here — only the masked configPrefix.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Antworten
-
200The tenant's inbound sources (masked config).
application/jsonobject -
403An API key is refused by class (
credential_not_accepted); an OIDC caller below admin getsinsufficient_role.
Register an inbound source
POST /inbound-sources
Registers an external source we RECEIVE signed event notifications from and returns it WITH the opaque stable publicSlug, the api-side inboundPath (/inbound/<slug>, in-cluster callers only) and the complete receiving address inboundUrl (null while this environment does not accept outside senders). For a signed source-type (hmac/shared_secret) the full verification secret is returned exactly once (retrievable later via /reveal-config). mode is fixed at creation to the caller's active context; a fetch_to_confirm source's fetchBaseUrl is SSRF-checked (an internal host ⇒ 400 ssrf_blocked). Honors the optional Idempotency-Key header (a retried registration does not create a second source minting a second secret). A retry of a registration that already succeeded answers 409 idempotency_replay_secret_withheld with the source it created, minus secret; read the secret again with /reveal-config.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
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 CreateInboundSource
Antworten
-
201Created. Body is the source plus (for signed types) the full plaintext secret.
application/jsonInboundSourceWithSecret -
400Validation error,
mode_mismatch,entity_mismatch, orssrf_blocked. -
403An API key is refused by class (
credential_not_accepted); an OIDC caller below admin getsinsufficient_role. -
409Idempotency-Key conflict, on a route whose success body carries a one-time secret.
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);idempotency_replay_secret_withheld— the first request bearing this key already SUCCEEDED. Nothing new was created. The secret it returned is shown only once and is never stored for a replay, so this answer carries the created resource inresourcewithout the secret. Do not retry it.application/jsonIdempotencyConflictOrSecretWithheldError
Get one inbound source (masked config)
GET /inbound-sources/{id}
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
Antworten
-
200The source (masked
configPrefix, never the plaintext).application/jsonInboundSource -
404Not found / not visible to the caller.
Update an inbound source (subscribedTypes / status)
PATCH /inbound-sources/{id}
mode, type and verificationMethod are NOT mutable — a body containing one (or any unknown field) is rejected 400. Audit-logged.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
Request-Body erforderlich
application/json UpdateInboundSource
Antworten
-
200The updated source.
application/jsonInboundSource -
400Validation error (incl. an attempt to change an immutable field).
-
404Not found / not visible.
Disable an inbound source (soft-delete)
DELETE /inbound-sources/{id}
Soft-delete: flips status='disabled' so the inbound-event log stays referentially intact. Audit-logged.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
Antworten
-
204Disabled.
-
404Not found / not visible.
Reveal the full verification config (audit-logged, repeatable)
POST /inbound-sources/{id}/reveal-config
Decrypts and returns the full plaintext verification config (the retrievable-secret discipline). Writes an audit-log entry on EVERY call.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
Antworten
-
200The full plaintext verification config.
application/jsonobject -
404Not found / not visible.
Rotate the verification secret (signed source-types only)
POST /inbound-sources/{id}/rotate-secret
Generates a new secret and returns it once; the old secret stops verifying immediately (no overlap). Only valid for a signed source-type (hmac/shared_secret); a fetch_to_confirm/mtls/ip_allowlist source has no rotatable secret (409 no_secret_to_rotate). Audit-logged.
⚠ There is NO overlap period: any sender still signing with the previous secret is refused on its very next request. Store the new secret wherever this source's sender runs before rotating, or rotate again once it is ready — a refused notification loses nothing, it leaves the invoice open.
Honors the optional Idempotency-Key header. ⭐ Use one: a rotation MINTS AND DESTROYS, so a retried request without a key rotates a SECOND time and invalidates the secret the first response handed back. With a key, a retry of a rotation that already succeeded rotates nothing and answers 409 idempotency_replay_secret_withheld with the source, minus the secret: the secret the first response handed back is the one that still verifies (read it again with /reveal-config). ⚠ Mint the key per intended rotation, never reuse one — the request digest is the resolved path plus the (empty) body, so a key is bound to ONE source.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
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 |
Antworten
-
200The source plus the new full plaintext secret.
application/jsonInboundSourceWithSecret -
404Not found / not visible.
-
409This verification method has no rotatable secret (
no_secret_to_rotate); or anIdempotency-Keyconflict —REQUEST_IN_PROGRESS(a rotation with this key is still running and will succeed; re-read the source rather than treating it as an error),IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY(the key was already used for a DIFFERENT source; nothing was rotated) oridempotency_replay_secret_withheld(the rotation with this key already succeeded;resourceis the source, without the secret).application/jsonIdempotencyConflictOrSecretWithheldError
Switch automatic booking on or off for ONE payment source
PUT /inbound-sources/{id}/auto-reconcile
⭐ NARROWS the company's standing authorisation for automatic booking to the sources an administrator has switched on. ⛔ It is NOT an authorisation and it grants nothing: a source can only ever book automatically while an active authorisation exists for the INVOICE's company, so this switch can only ever REDUCE what that authorisation already permits.
⛔ enabled is the DESIRED state, never a flip, and there is deliberately no toggle verb: two identical requests leave exactly one state. On this control the second press of a relative verb is the one that silently switches a source OFF.
Switching ON is refused 422 while the source was registered AFTER the company's authorisation was given — arming it would extend automatic booking to a source that did not exist when the acknowledgement was made. Confirm the authorisation again for the company (PUT /settings/auto-reconcile with reaffirm: true), then switch the source on. ⭐ Switching ON with NO authorisation at all is ACCEPTED: there is no grant instant to compare against, and the response says the source will not book yet.
⛔ Switching OFF is NEVER refused — not for coverage, not for a missing authorisation, not for a disabled source. Switching off a source that is already off is a 200. Stopping must never be harder than starting.
Audit-logged on every accepted call, with the acting person. ⛔ Not idempotency-wrapped: an absolute-value PUT is idempotent by construction, and a replay would return a recorded body without writing the audit entry this act must always write.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
Request-Body erforderlich
application/json SetInboundSourceAutoReconcile
Antworten
-
200The updated source, carrying the resolved
autoReconcilestate.application/jsonInboundSource -
400Validation error — an unknown property, or a non-boolean
enabled.application/jsonValidationError -
403An API key is refused by class (
credential_not_accepted); an OIDC caller below admin getsinsufficient_role. -
404Not found / not visible.
-
422auto_reconcile_source_not_covered— this source was registered after the company's active authorisation was given, so switching it on would extend automatic booking to a source that did not exist when the acknowledgement was made.detailsnamessourceCreatedAt,grantedAtandcurrentDisclosureVersion. NOTHING WAS WRITTEN. Confirm the authorisation again onPUT /settings/auto-reconcile.application/jsonValidationError
Testanfrage simulieren — sandbox-locked synthetic received-event
POST /inbound-sources/{id}/simulate
SANDBOX-LOCKED (test mode ONLY — a live key/session is 409 sandbox_test_mode_only before any work). Fires a server-side SIGNED synthetic received-event against a chosen TEST invoice and flows it through the REAL receiver pipeline (verify → dedup → match → suggest/auto), so an ISV watches the timeline without wiring their sender. Structurally incapable of touching a LIVE invoice: the source is resolved under the test context and the pipeline scopes candidates to mode='test'. Only signed source-types (hmac/shared_secret) can be simulated (else 400 unsupported_method). Poll GET /inbound-events/:id (returned eventId) for the resulting step timeline.
- Erforderlicher Scope
- keiner
- Zugang
- nur mit angemeldeter Sitzung – ein API-Schlüssel wird abgelehnt
(
oidc-only)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | Inbound source id. |
Request-Body erforderlich
application/json SimulateInboundEvent
Antworten
-
202The synthetic event ran the pipeline.
application/jsonobject -
400Validation error, or
unsupported_method(an edge-verified source). -
403An API key is refused by class (
credential_not_accepted); an OIDC caller below admin getsinsufficient_role. -
404Not found / not visible / disabled source.
-
409Not in test mode (
sandbox_test_mode_only).
Public signed inbound-event receiver (the security spine)
POST /inbound/{slug}
SIGNATURE-authed inbound receiver: the caller is the external source, NOT a user — an explicit carve-out of the default-deny gate, authenticated per the source's verification method (hmac: HMAC-SHA256 over {Webhook-Id}.{Webhook-Timestamp}.{raw body} via Webhook-Signature; shared_secret: the opaque slug + an X-Inbound-Token header). VERIFIES BEFORE any side-effect; logs the arrival to the append-only inbound-event log; dedups on the source and its deduplication key; dispatches ONLY a verified, parseable event to its typed handler. A forged/unsigned POST is logged rejected_* and NEVER dispatched — it can never flip an invoice. Response: 404 unknown/disabled slug · 401 bad/absent signature · 200 stale (stop retrying) / duplicate / processed. Mounted only when inbound webhooks; public exposure is a gated infrastructure step (until then the endpoint is not publicly reachable).
- Erforderlicher Scope
- keiner
- Zugang
- ohne Anmeldung
(
public)
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
slug erforderlich | Pfad | string | The opaque, stable per-source slug (from source registration). |
Webhook-Id | Header | string | Stable per-message id, identical across every retry of the same message. It is an HMAC input, so it cannot be altered in transit; dedupe on it. ( |
Webhook-Signature | Header | string |
|
Webhook-Timestamp | Header | string | Unix seconds. It is INSIDE the signature, so a captured message cannot be re-stamped and replayed — reject anything outside your tolerance (we use 5 minutes). ( |
X-Inbound-Token | Header | string | The shared token we assigned ( |
Request-Body erforderlich
application/json object
Antworten
-
200Verified + logged (processed/ignored), a deduped redelivery, or a stale-but-signed replay — the sender must stop retrying. Body is metadata-only (
{ outcome, eventId? }). -
401Missing/invalid signature — rejected before any side-effect, never dispatched.
-
404Unknown or disabled slug (no work performed).
List received inbound events (cursor-paginated, metadata-only)
GET /inbound-events
Readable by readonly and above (the OIDC rank mirrors payments:read, which IS in READONLY_SCOPES — the Steuerberater watches the money log). Cursor-paginated, MODE-SCOPED (a test read never returns live events, and vice versa), filterable by type/outcome/signatureStatus/from/to. METADATA-ONLY: no raw payload; the matched invoice surfaces as its Beleg-Nr. only (never the customer name). Keyset over (received_at, id) DESC.
- Erforderlicher Scope
-
payments:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
type | Query | string | |
outcome | Query | string | One outcome, or a COMMA-SEPARATED SET of them, so a "problems" view is one request over the four |
sourceId | Query | string (uuid) | Return only events received by this one source. |
signatureStatus | Query | string | Werte: "valid", "invalid", "missing", "stale" |
from | Query | string (date-time) | |
to | Query | string (date-time) | |
cursor | Query | string | Opaque keyset cursor from a prior page's |
limit | Query | integer | Standard: 50 Bereich: ≥ 1, ≤ 100 |
Antworten
-
200A page of received events (metadata-only) + an opaque
nextCursor.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 -
403Caller is not an active member (
insufficient_role), or an api key lackspayments:read. ⛔ NOT an admin bar: reads are open toreadonlyand above.
One inbound event + its processing-step timeline (metadata-only)
GET /inbound-events/{id}
The metadata-only processing-step timeline (Signatur geprüft → Ereignis erkannt → Duplikat geprüft → Beleg zugeordnet → Aktion ausgeführt). The matched invoice is shown as its Beleg-Nr. only (never the customer name); no raw payload is dumped. Mode-scoped (a foreign-mode id is 404).
- Erforderlicher Scope
-
payments:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The inbound-event id. |
Antworten
-
200The event metadata + the derived step timeline.
application/jsonInboundEventDetail -
404Not found / not visible to the caller.
Re-dispatch (replay) a logged inbound event
POST /inbound-events/{id}/reprocess
Admin-gated "Erneut verarbeiten": re-dispatch a LOGGED event through the handler registry (async 202; poll GET /inbound-events/:id for the updated timeline). Reconstructs the event from the retained metadata (the raw body is never stored). IDEMPOTENT + safe: a forged/unverified event is NEVER re-dispatched (stays rejected — the invariant under replay); an already-processed event is a no-op (no second payment/allocation); a re-drivable event (pending_confirmation/rejected_*) re-runs match→transition, reusing the provenance payment (no double booking).
- Erforderlicher Scope
-
payments:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The inbound-event id to re-dispatch. |
Antworten
-
202The replay ran; the terminal outcome is returned inline.
application/jsonobject -
403Caller lacks the admin role.
-
404Not found / not visible to the caller.
Human-confirm the suggested payment match (Zahlungsabgleich)
POST /inbound-events/{id}/confirm
Human-confirm flip, open to member and above; readonly is refused, mirroring payments:write being deliberately ABSENT from READONLY_SCOPES. For a pending_confirmation event (the matcher suggested it). In ONE scoped transaction: (1) NOTIFY-THEN-FETCH — re-confirms the authoritative amount/status against our source-of-truth (a mismatch/none aborts with NO flip, the event stays pending_confirmation); (2) allocates the payment + recomputes payment_state, flipping status issued → paid ONLY when the invoice is exactly paid (a partial stays issued + partially_paid); (3) stamps outcome='processed', writes an audit-log entry, and emits the two-sided outbound event (invoice.paid on a full flip, payment.partial on a partial, payment.reconciled on an overpayment) through the webhook delivery infra. Idempotent — confirming an already-processed event is a no-op. A verified suggestion still flips nothing without this human confirm + notify-then-fetch (the invariant's second layer). Admin/owner only.
- Erforderlicher Scope
-
payments:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The inbound-event id (a |
Antworten
-
200Confirmed (or an idempotent replay of an already-
processedevent). Body is metadata-only:{ outcome: 'processed', invoiceId, paymentState?, statusFlipped?, emitted? }.application/jsonobject -
403Insufficient role (admin/owner required).
-
404No such event in the caller's entity/mode scope.
-
409Not confirmable (
not_confirmable) — the event is not inpending_confirmation, has no recorded payment/invoice — or the notify-then-fetch re-confirmation failed (confirmation_failed): NO allocation, NO flip.
Record a payment against an invoice
POST /invoices/{id}/payments
Appends a payment FACT to the invoice's sub-ledger and finalizes its derived state. This is a ledger entry, never a status write — there is no mark-paid verb anywhere in this API; „mark as paid" is a UI shape over this endpoint with the body {"mode":"settle_remaining"}, which carries no money value at all. status/paymentState/amountPaidMinor move only through the single shared recompute, so a bank feed and a human entry produce the identical transition and the identical webhook.
mode is REQUIRED and the body is a tagged union. An absent or unreadable intent is 400 payment_intent_required and allocates NOTHING: on an endpoint that moves money, an omitted field must never resolve to the most consequential value. settle_remaining allocates the invoice's whole open amount computed SERVER-side, so it cannot overpay by construction and no number crosses the wire. exact_amount allocates the integer number of minor units you state — the mode for an early-payment discount, a part payment, or a rounding difference, where the amount that arrived is not the amount that was invoiced.
The open amount nets credit notes, and both modes measure against the same figure. A €1 000 invoice carrying a €300 partial credit note has an open amount of €700 — that is what settle_remaining books, and that is what an exact_amount is compared against. It is the same derivation GET /invoices/{id}/payments publishes as summary.openAmountMinor, so the number you are shown, the number a refusal quotes and the number the server books can never disagree.
No allocation of 0 is ever inserted. "€0 arrived" is not a fact, and this ledger records facts — an already-settled invoice is 409 invoice_already_settled, never a zero-amount row. Concurrent requests are serialised by a SELECT … FOR UPDATE on the invoice, so the loser re-reads the winner's committed amount and lands that same 409.
Idempotency-Key is REQUIRED (400 idempotency_key_required when absent). Payments have no natural exactly-once guard — two genuine €50 payments on one invoice on one day are legal — so the server cannot tell a double-submit from a real second payment; only the key can. The digest covers the RESOLVED PATH, so one key reused against a different invoice is a different request, not a replay. ⚠ A 4xx consumes its Idempotency-Key: a corrected retry must carry a fresh one. Mint one key per intent, when the intent is formed, and re-mint it when the intent changes.
⚠ amountMinor is an INTEGER in MINOR UNITS, and a value that is not one is refused rather than repaired. 98000 is 980,00 EUR. A string ("98000"), a fractional number (980.5) or an amount typed with a decimal comma is 400 payment_amount_required, naming the field — because those are exactly the shapes a mangled money input arrives in, and a server that quietly rounds or rescales one writes the repair onto an append-only ledger that also drives dunning. Normalise the value where the person typed it. An amount of 0 or below is a different answer: 422 payment_amount_out_of_range.
An overpayment is possible but never accidental. An amountMinor above the open amount is 409 payment_exceeds_open_amount, whose details.openAmountMinor states the true remaining balance; repeat the request with allowOverpayment: true if the larger amount is what really arrived. An invoice with NO open amount left is 409 invoice_already_settled in both modes — that refusal is taken before the stated amount is consulted at all.
The published enum below is exactly the set of modes the server executes, and CI fails if publishing and executing ever drift apart.
Scope is payments:write, and it is now SETTLED. It shipped on a transitional documents:write grant, which no longer reaches this route: recording a payment EXTINGUISHES a receivable, which is a different authority from creating one, so a bookkeeping integration needs payments:write and nothing more. Scopes are additive — this route's requirement can be widened but will never be tightened again.
⭐ The payment event family is a SEQUENCE, not a set of terminal flags. invoice.paid may be delivered more than once for one invoice; it may be followed by payment.reversed; and that may be followed by invoice.paid again. Apply them in created_at order — not arrival order — and treat the latest created_at as current. Two events with different ids are two distinct occurrences even when type is identical; invoice.paid is not a latch, and the absence of a further event is not confirmation.
- Erforderlicher Scope
-
payments:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The issued invoice the payment settles. |
Idempotency-Key erforderlich | Header | string | REQUIRED on this route. A replay (same key, same resolved path, same body, after the first completed) returns the recorded response verbatim without re-executing. A key reused with a different body or against a different invoice is Länge: max. 255 |
Request-Body erforderlich
application/json object
Antworten
-
201The recorded payment and the invoice's resulting state (or, on an Idempotency-Key replay, the recorded response).
application/jsonobject -
400payment_intent_required(no body / unparseable /modeabsent — nothing is allocated),payment_intent_unknown(details.received+details.supported),payment_mode_not_enabled(a recognised mode this deployment does not execute),payment_intent_conflict(settle_remainingcarryingamountMinor/allowOverpayment—details.conflictingFields),payment_amount_required(exact_amountwhoseamountMinoris absent, a string, fractional or out of safe-integer range —details.field), oridempotency_key_required.application/jsonPaymentIntentError -
403Caller lacks the
payments:writescope (areadonlymember 403s here). -
404Invoice not in scope, or manual payment entry is not enabled.
-
409invoice_not_issued(draft/scheduled — no receivable yet),invoice_already_settled(the remaining open amount is 0; also the honest outcome for the loser of a concurrent-settle race),invoice_cancelled_by_correction(an issued/paid 384, or a 381 that reverses this invoice IN FULL, settles it by document rather than by money),correction_chain_indeterminate(one of this invoice's own cancellation documents has itself been cancelled, so the remaining amount cannot be determined and there is no defensible figure to book),payment_exceeds_open_amount(anexact_amountabove the invoice's open amount with noallowOverpayment: true—details.openAmountMinorstates the true remaining balance). AlsoIDEMPOTENCY_*on key conflicts. Nothing is written in any of these cases. NB: a 381 that reverses only PART of this invoice does NOT refuse the request — it REDUCES the open amount, which is thengross − already allocated − the same-currency partial credit notes. That is the figuresettle_remainingbooks and the figure anexact_amountis measured against. -
422payment_amount_out_of_range(anamountMinorof0or below — read and understood, and not a receipt; to take a recorded payment back, reverse its allocation), orvalidation_errorfor avalueDateoutside[2000-01-01, today + 1 year].
The per-invoice payment ledger
GET /invoices/{id}/payments
The invoice's payment sub-ledger plus a summary. This is not a convenience read. The reversal is addressed by allocationId, and an AR-staff member cannot get one from the audit log (owner-role-only and metadata-only) — only from here. Without this read the reversal endpoint is unreachable in practice.
entries is ONE ROW PER ALLOCATION, discriminated by kind. A reversal is its OWN entry (negative amountMinor), never collapsed into a flag on its target — the projection therefore is the ledger, and Σ entries[].amountMinor === summary.amountPaidMinor is an invariant a client can assert. Its target additionally carries reversed: true and reversalAllocationId, so an "undo" affordance belongs on rows where kind === 'payment' && !reversed. A reversal entry carries its stated reversalReason and, when one was given, its reversalNote. summary.currency names the currency of every amount in the response.
summary.openAmountMinor is totalGross − amountPaid − the same-currency partial credit notes (issued or paid credit notes that reverse part of this invoice), derived by the SAME code path the open-items worklist uses — and it is the only sanctioned source for the amount a confirm step states before recording a payment. Never client-computed.
origin is manual for a payment a person recorded through this API and inbound for one booked from a received payment notification, whether a person confirmed it or it was booked automatically (see bookedAutomatically).
Scope is payments:read and is now settled; the transitional documents:read grant no longer reaches it. A readonly member still holds payments:read, so the ledger view the human path had before this change is unchanged.
- Erforderlicher Scope
-
payments:read - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The invoice whose payment ledger to read. |
Antworten
-
200The ledger projection (a
200also tells a client the surface is enabled).application/jsonInvoicePaymentLedger -
403Caller lacks the
payments:readscope. -
404Invoice not in scope, or manual payment entry is not enabled.
Reverse a recorded payment allocation
POST /invoices/{id}/payment-allocations/{allocationId}/reversal
Corrects a mis-recorded payment by appending a compensating negative allocation that points at the original. Payments and their allocations are append-only, enforced by the database AND RESTRICTIVE RLS, so an UPDATE or DELETE is impossible, not merely discouraged — the payment ledger corrects itself the same way the invoice ledger does, by a new entry that references the old one. The payments row is never touched: a receipt that turned out never to have arrived stays on the ledger with a NET ALLOCATION OF ZERO, which is how a real ledger records it.
Keyed on the ALLOCATION, not the payment, because one payment may carry several allocations (a Sammelüberweisung across invoices, or several against one). Source the allocationId from GET /invoices/{id}/payments. Reversing an inbound allocation is explicitly allowed — a bank feed can be wrong too, and a Rücklastschrift makes it routine.
The invoice un-pays. If the reversal takes the invoice back below fully paid, status moves paid → issued and it re-enters the open-items and dunning worklists at its full amount. Without that, the invoice would read paid while genuinely unpaid and be invisible to every worklist.
At most ONE reversal per allocation, ever, enforced by a UNIQUE index as well as by the pre-check — two reversals would drive amountPaidMinor negative and re-dun the invoice for more than it is worth. A reversal is not itself reversible: the honest expression of "un-reverse" is a new POST /invoices/{id}/payments, because un-reversing is arithmetically a fresh receipt carrying no evidence that money arrived.
reason.category is REQUIRED; note (≤ 280 chars) is optional and is NEVER rendered to a customer. Both are readable back on GET /invoices/{id}/payments (reversalReason and reversalNote on the reversal entry); the note is never rendered to the customer and never appears in a PDF, an XML, a webhook payload or an e-mail.
Idempotency-Key is REQUIRED, same contract as the write.
Emits payment.reversed, once per reversed allocation — so a second reversal on the same invoice emits its own event, and a re-payment after a reversal emits a further invoice.paid. Apply the payment family in created_at order and treat the latest created_at as current.
Scope is payments:write — the same settled requirement as the write it undoes. Whoever may record a payment may correct one; splitting the two would leave a caller able to book a receipt it cannot take back.
- Erforderlicher Scope
-
payments:write - Zugang
- API-Schlüssel oder angemeldete Sitzung
Parameter
| Parameter | In | Typ | Beschreibung |
|---|---|---|---|
id erforderlich | Pfad | string (uuid) | The invoice the allocation belongs to. It is part of the lookup, not decoration. |
allocationId erforderlich | Pfad | string (uuid) | The allocation to reverse, as returned by |
Idempotency-Key erforderlich | Header | string | REQUIRED on this route — same contract as Länge: max. 255 |
Request-Body erforderlich
application/json object
Antworten
-
201The compensating entry and the invoice's resulting state.
application/jsonobject -
400validation_error(a missing/invalidreason) oridempotency_key_required. -
403Caller lacks the
payments:writescope. -
404allocation_not_found— ONE code for all three misses: the allocation belongs to another entity, to another mode, or to a DIFFERENT INVOICE than the one in the path. Distinguishing them would be an existence-and-amount oracle. Also returned when manual payment entry is not enabled. -
409allocation_already_reversed(at most one reversal per allocation, ever — returned by the pre-check AND by the unique index on a race, never a 500) orallocation_is_a_reversal(a reversal is not itself reversible). AlsoIDEMPOTENCY_*on key conflicts. Nothing is written.
Datenmodelle
Jedes Datenmodell dieser Gruppe einmal, mit allen Feldern – auch den verschachtelten. Verschachtelte Objekte sind aufklappbar; ein benannter Typ verweist auf seinen eigenen Abschnitt.
InboundSource object
A registered inbound source (the inverse of a webhook endpoint). The verification secret is NEVER present here — only the masked configPrefix.
| Feld | Typ | Beschreibung |
|---|---|---|
id erforderlich | string (uuid) | |
tenantId | string (uuid) | |
entityId erforderlich | string (uuid) | null | NULL would be a whole-tenant source (deferred; always the active entity today). |
mode erforderlich | string | Set at creation, never mutable. Werte: "live", "test" |
type erforderlich | string | Werte: "payment_provider", "bank_finapi", "generic_hmac" |
verificationMethod erforderlich | string | Werte: "hmac", "fetch_to_confirm", "mtls", "ip_allowlist", "shared_secret" |
configPrefix erforderlich | string | Masked verification-config identifier ( |
subscribedTypes erforderlich | Array<string> | |
publicSlug erforderlich | string | The opaque, stable |
inboundPath veraltet | string | The api-side receiver route |
inboundUrl | string | null | The COMPLETE receiving address a sender is configured with, e.g. |
status erforderlich | string | Werte: "active", "disabled" |
createdBy erforderlich | string (uuid) | |
createdAt erforderlich | string (date-time) | |
updatedAt erforderlich | string (date-time) | |
autoReconcile erforderlich | InboundSourceAutoReconcile | |
lastEventAt | string (date-time) | null | When this source last RECEIVED anything; |
Verwendet von:
GET /inbound-sources , GET /inbound-sources/{id} , PATCH /inbound-sources/{id} , PUT /inbound-sources/{id}/auto-reconcile
CreateInboundSource object
| Feld | Typ | Beschreibung |
|---|---|---|
type erforderlich | string | Werte: "payment_provider", "bank_finapi", "generic_hmac" |
verificationMethod erforderlich | string | Werte: "hmac", "fetch_to_confirm", "mtls", "ip_allowlist", "shared_secret" |
subscribedTypes erforderlich | Array<string> | Free-form event types (a new type is a new registered handler). Elemente: min. 1 |
mode | string | Optional; must equal the caller's active mode (else 400). Fixed at creation. Werte: "live", "test" |
entityId | string (uuid) | Optional; must equal the caller's active entity (else 400). |
fetchBaseUrl | string | Only for |
Verwendet von:
POST /inbound-sources
InboundSourceWithSecret object
An InboundSource plus the full plaintext secret (signed source-types only), returned once. Retrievable later via /reveal-config.
| Feld | Typ | Beschreibung |
|---|---|---|
id erforderlich | string (uuid) | |
tenantId | string (uuid) | |
entityId erforderlich | string (uuid) | null | NULL would be a whole-tenant source (deferred; always the active entity today). |
mode erforderlich | string | Set at creation, never mutable. Werte: "live", "test" |
type erforderlich | string | Werte: "payment_provider", "bank_finapi", "generic_hmac" |
verificationMethod erforderlich | string | Werte: "hmac", "fetch_to_confirm", "mtls", "ip_allowlist", "shared_secret" |
configPrefix erforderlich | string | Masked verification-config identifier ( |
subscribedTypes erforderlich | Array<string> | |
publicSlug erforderlich | string | The opaque, stable |
inboundPath veraltet | string | The api-side receiver route |
inboundUrl | string | null | The COMPLETE receiving address a sender is configured with, e.g. |
status erforderlich | string | Werte: "active", "disabled" |
createdBy erforderlich | string (uuid) | |
createdAt erforderlich | string (date-time) | |
updatedAt erforderlich | string (date-time) | |
autoReconcile erforderlich | InboundSourceAutoReconcile | |
lastEventAt | string (date-time) | null | When this source last RECEIVED anything; |
secret | string | The full plaintext verification secret (present only for |
Verwendet von:
POST /inbound-sources , POST /inbound-sources/{id}/rotate-secret
IdempotencyConflictOrSecretWithheldError object
| Feld | Typ | Beschreibung |
|---|---|---|
error erforderlich | string | Werte: "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY", "REQUEST_IN_PROGRESS", "idempotency_replay_secret_withheld" |
message | string | |
resource | object | Only with |
Verwendet von:
POST /inbound-sources , POST /inbound-sources/{id}/rotate-secret
UpdateInboundSource object
Mutable fields only. mode/type/verificationMethod are intentionally absent — including one (or any unknown field) is rejected 400.
- Mindestens 1 Feld(er)
| Feld | Typ | Beschreibung |
|---|---|---|
subscribedTypes | Array<string> | Elemente: min. 1 |
status | string | Werte: "active", "disabled" |
Verwendet von:
PATCH /inbound-sources/{id}
SetInboundSourceAutoReconcile object
⛔ ONE property, and a body this contract does not recognise is a 400 rather than a silent drop. On a request whose whole subject is „may this source move money with nobody present", accepting a property we do not understand would be accepting a configuration we cannot describe.
| Feld | Typ | Beschreibung |
|---|---|---|
enabled erforderlich | boolean | The DESIRED state, not a flip. Two identical requests leave exactly one state and record two audit entries. |
Verwendet von:
PUT /inbound-sources/{id}/auto-reconcile
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:
PUT /inbound-sources/{id}/auto-reconcile , GET /inbound-events
SimulateInboundEvent object
The synthetic event to fire at a chosen TEST invoice. All fields optional; invoiceNumber/structuredReference drive the explicit-reference match and amountMinor the exact-amount classification.
| Feld | Typ | Beschreibung |
|---|---|---|
invoiceNumber | string | The target TEST invoice's Beleg-Nr. |
amountMinor | integer | The received amount in minor units. |
currency | string | Standard: "EUR" Länge: min. 3, max. 3 |
remittanceInfo | string | Free-text Verwendungszweck. |
structuredReference | string | A structured reference (wins over invoiceNumber). |
valueDate | string | |
debtorIban | string | Hashed before storage (never stored raw). |
dedupKey | string | Optional idempotency key; synthesized if omitted. |
type | string | Standard: "payment_received" |
Verwendet von:
POST /inbound-sources/{id}/simulate
InboundEventListItem object
One received-event row — matching-signal METADATA only (HARD FENCE + GDPR). The matched invoice is its Beleg-Nr. only, never the customer name.
| Feld | Typ | Beschreibung |
|---|---|---|
id erforderlich | string (uuid) | |
sourceId erforderlich | string (uuid) | |
type erforderlich | string | |
receivedAt erforderlich | string (date-time) | |
signatureStatus erforderlich | string | Werte: "valid", "invalid", "missing", "stale" |
verified erforderlich | boolean | |
outcome erforderlich | string | null | Werte: "processed", "ignored_duplicate", "ignored_already_paid", "rejected_signature", "rejected_no_match", "rejected_stale", "rejected_unparseable", "pending_confirmation" |
matchedInvoiceNumber | string | null | The matched invoice's Beleg-Nr. ONLY — never the customer name. |
sourceType | string | null | The reporting source's KIND, so a reader can label the source column without holding the admin-only source listing. ⛔ Never the receiving address, the masked prefix or the secret. Null only if the source was removed after the event arrived. |
remittanceReference | string | null | |
reconciliation erforderlich | InboundReconciliation | Where the money stands NOW, derived from the append-only ledger. ⛔ Not a second reading of |
Verwendet von:
GET /inbound-events
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 /inbound-events
InboundEventDetail object
One event's metadata-only detail — the list-item fields plus the derived processing-step timeline. Beleg-Nr. only, never a customer name / raw body.
| Feld | Typ | Beschreibung | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id erforderlich | string (uuid) | |||||||||||||
sourceId erforderlich | string (uuid) | |||||||||||||
type erforderlich | string | |||||||||||||
receivedAt erforderlich | string (date-time) | |||||||||||||
signatureStatus erforderlich | string | Werte: "valid", "invalid", "missing", "stale" | ||||||||||||
verified erforderlich | boolean | |||||||||||||
outcome erforderlich | string | null | Werte: "processed", "ignored_duplicate", "ignored_already_paid", "rejected_signature", "rejected_no_match", "rejected_stale", "rejected_unparseable", "pending_confirmation" | ||||||||||||
matchedInvoiceNumber | string | null | The matched invoice's Beleg-Nr. ONLY — never the customer name. | ||||||||||||
sourceType | string | null | The reporting source's KIND, so a reader can label the source column without holding the admin-only source listing. ⛔ Never the receiving address, the masked prefix or the secret. Null only if the source was removed after the event arrived. | ||||||||||||
remittanceReference | string | null | |||||||||||||
reconciliation erforderlich | InboundReconciliation | Where the money stands NOW, derived from the append-only ledger. ⛔ Not a second reading of | ||||||||||||
matchedInvoiceId | string (uuid) | null | |||||||||||||
amountMinor | integer | null | The credit's amount in MINOR UNITS, from the provenance payment row. Null when nothing matched. | ||||||||||||
currency | string | null | |||||||||||||
valueDate | string (date) | null | |||||||||||||
remittanceInfo | string | null | The remittance text VERBATIM as the bank sent it — distinct from | ||||||||||||
summary | InvoicePaymentSummary | null | The matched invoice's money. ⭐ Null EXACTLY when nothing matched, which is what lets a confirm control be withheld on a fact rather than on a convention. | ||||||||||||
timeline erforderlich | Array<object> | Signatur geprüft → Ereignis erkannt → Duplikat geprüft → Beleg zugeordnet → Aktion ausgeführt → Zahlung verbucht. ⭐ The SIXTH step carries the derived booking state ( Felder
|
Verwendet von:
GET /inbound-events/{id}
InvoicePaymentState object
The invoice's resulting state after a payment or a reversal. It NESTS rather than flattening, deliberately: a future tenant-level payment endpoint settles several invoices from one arrival and must answer invoices: [ … ], so flattening now would make status mean two different things at the same JSON depth.
| Feld | Typ | Beschreibung |
|---|---|---|
id erforderlich | string (uuid) | |
status erforderlich | string | The invoice lifecycle status. It moves in exactly two directions here — Werte: "draft", "scheduled", "issued", "paid", "void" |
paymentState erforderlich | string | Werte: "open", "partially_paid", "paid", "overpaid" |
amountPaidMinor erforderlich | integer | Σ of every allocation on this invoice, in minor units (reversals included). |
openAmountMinor erforderlich | integer | What is still owed on this invoice, in minor units: |
Verwendet von:
POST /invoices/{id}/payments , POST /invoices/{id}/payment-allocations/{allocationId}/reversal
PaymentIntentError object
The 400s of the payment write. They are distinct registered codes rather than one generic validation_error because this refusal is a SAFETY response, not a schema complaint: it must be greppable in a log and branchable in a client. The rule behind the split — 400 = "I cannot tell what you are asking for"; 422 = "I understand the ask, the value is out of range." So an amountMinor that is absent, sent as a string, or fractional is payment_amount_required (unreadable); an amount of 0 or below is 422 payment_amount_out_of_range (read, and not a receipt). Neither is ever repaired into a plausible number.
| Feld | Typ | Beschreibung | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
error erforderlich | string | Werte: "payment_intent_required", "payment_intent_unknown", "payment_mode_not_enabled", "payment_intent_conflict", "payment_amount_required", "idempotency_key_required" | |||||||||||||||
details | object | Felder
|
Verwendet von:
POST /invoices/{id}/payments
InvoicePaymentLedger object
| Feld | Typ | Beschreibung |
|---|---|---|
summary erforderlich | InvoicePaymentSummary | |
entries erforderlich | Array<InvoicePaymentLedgerEntry> | Chronological, oldest first. |
Verwendet von:
GET /invoices/{id}/payments
InboundSourceAutoReconcile object
Whether this source books matched payments automatically, and why not when it does not.
⛔ state is RESOLVED BY THE PLATFORM and must never be recomputed by a client. „Will the next matched payment from this source be booked without anyone present?" is the same class of question as effective on the automatic-booking setting, and two implementations of it would be discovered to disagree by a customer. The inputs are published beside it so a surface can EXPLAIN the state without deriving it.
| Feld | Typ | Beschreibung |
|---|---|---|
enabled erforderlich | boolean | The administrator's switch for this source. |
state erforderlich | string |
Werte: "auto", "off", "needs_reaffirmation", "blocked_no_consent", "blocked_platform", "source_disabled" |
consentPresent erforderlich | boolean | An active authorisation exists for this source's company and environment. |
coveredByConsent erforderlich | boolean | This source already existed when that authorisation was given. |
platformEnabled erforderlich | boolean | The environment's own capability for automatic booking. |
Verwendet von:
GET /inbound-sources , POST /inbound-sources , GET /inbound-sources/{id} , PATCH /inbound-sources/{id} , POST /inbound-sources/{id}/rotate-secret , PUT /inbound-sources/{id}/auto-reconcile
InboundReconciliation object
⭐ WHERE THE MONEY STANDS NOW for one received event — DERIVED from the append-only payment sub-ledger and stored nowhere (no column, no CHECK, no migration).
⛔ IT IS A DIFFERENT QUESTION FROM outcome, AND THE TWO MUST NOT BE PRESENTED AS TWO VERDICTS ABOUT ONE THING. outcome answers what we did with the notification when it arrived, and keeps saying processed after a reversal because that is what the receiver decided and it is still true. state answers where the money stands. A human reversing an allocation moves the second and never the first — which is why outcome was NOT given a terminal value: it is a published query FILTER, so a terminal value would make a correctly-processed event leave the „verbucht" set and appear in neither filter.
| Feld | Typ | Beschreibung |
|---|---|---|
state erforderlich | string |
Werte: "not_booked", "standing", "partially_reversed", "fully_reversed" |
grossAllocatedMinor erforderlich | integer | Minor units. The sum of the NON-REVERSING allocations — what was originally booked. |
netAllocatedMinor erforderlich | integer | Minor units. The sum of ALL allocations, compensating (negative) rows included. Equal to |
bookedAutomatically erforderlich | boolean | True iff the AUTOMATIC rail booked at least one of this event's allocations — i.e. nobody pressed a button. It is what makes the acknowledgement's „these bookings are attributed to the system, not to a person" checkable on the event itself rather than taken on trust. |
Verwendet von:
GET /inbound-events , GET /inbound-events/{id}
InvoicePaymentSummary object
One invoice's money, as the PAYMENT SUB-LEDGER states it. ⛔ Minor-unit integers, not decimal strings: this subsystem stores BIGINT cents (0070) and converts once at the boundary, so publishing decimals here would add a second conversion on the very screens that compare two of these figures. Every surface that states the balance before money is booked publishes it under this one $ref.
| Feld | Typ | Beschreibung |
|---|---|---|
totalGrossMinor erforderlich | integer | |
amountPaidMinor erforderlich | integer | |
openAmountMinor erforderlich | integer | The remaining balance, in minor units. ⛔ There is exactly ONE authority for this figure — |
paymentState erforderlich | string | Werte: "open", "partially_paid", "paid", "overpaid" |
currency | string | The invoice's ISO 4217 currency code (for example |
Verwendet von:
GET /inbound-events/{id} , GET /invoices/{id}/payments
InvoicePaymentLedgerEntry object
ONE ROW PER ALLOCATION. A reversal is its own entry with a negative amountMinor, never collapsed into a flag on its target — so the projection is the ledger and Σ amountMinor === summary.amountPaidMinor holds. Place an "undo" affordance on rows where kind === 'payment' && !reversed.
| Feld | Typ | Beschreibung |
|---|---|---|
allocationId erforderlich | string (uuid) | The reversal's key for this row. |
paymentId erforderlich | string (uuid) | |
kind erforderlich | string | Werte: "payment", "reversal" |
amountMinor erforderlich | integer | Minor units. NEGATIVE on a |
valueDate | string (date) | null | The date the money arrived, from the payment fact. |
recordedAt erforderlich | string (date-time) | When the entry was booked. |
origin erforderlich | string |
Werte: "manual", "inbound" |
bookedAutomatically erforderlich | boolean | ⭐ True iff the AUTOMATIC inbound rail booked this entry — i.e. nobody pressed a button.
It exists because |
reversed erforderlich | boolean | True on a |
reversalAllocationId | string (uuid) | On a reversed |
reversesAllocationId | string (uuid) | On a |
reversalReason | string | On a Werte: "wrong_amount", "wrong_invoice", "never_received", "duplicate_entry", "other" |
reversalNote | string | On a Länge: max. 280 |
Verwendet von:
GET /invoices/{id}/payments
Nach oben · Alle Ressourcengruppen · OpenAPI-Spezifikation (YAML)