Zahlungsabgleich

Zahlungseingänge empfangen und automatisch den passenden Rechnungen zuordnen.

Inbound payment reconciliation: sources, received events, and matching.

18 Operationen · Basis-URL https://api.fortlauf.de/v1

Operationen

  1. GET /inbound-sources/summary How many inbound sources are configured (counts only)
  2. GET /inbound-sources List inbound sources for the active tenant/entity
  3. POST /inbound-sources Register an inbound source
  4. GET /inbound-sources/{id} Get one inbound source (masked config)
  5. PATCH /inbound-sources/{id} Update an inbound source (subscribedTypes / status)
  6. DELETE /inbound-sources/{id} Disable an inbound source (soft-delete)
  7. POST /inbound-sources/{id}/reveal-config Reveal the full verification config (audit-logged, repeatable)
  8. POST /inbound-sources/{id}/rotate-secret Rotate the verification secret (signed source-types only)
  9. PUT /inbound-sources/{id}/auto-reconcile Switch automatic booking on or off for ONE payment source
  10. POST /inbound-sources/{id}/simulate Testanfrage simulieren — sandbox-locked synthetic received-event
  11. POST /inbound/{slug} Public signed inbound-event receiver (the security spine)
  12. GET /inbound-events List received inbound events (cursor-paginated, metadata-only)
  13. GET /inbound-events/{id} One inbound event + its processing-step timeline (metadata-only)
  14. POST /inbound-events/{id}/reprocess Re-dispatch (replay) a logged inbound event
  15. POST /inbound-events/{id}/confirm Human-confirm the suggested payment match (Zahlungsabgleich)
  16. POST /invoices/{id}/payments Record a payment against an invoice
  17. GET /invoices/{id}/payments The per-invoice payment ledger
  18. POST /invoices/{id}/payment-allocations/{allocationId}/reversal Reverse 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

  • 200

    Counts only.

    application/json object

  • 403

    An OIDC caller who is not an active member (insufficient_role), or a key without payments: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

  • 200

    The tenant's inbound sources (masked config).

    application/json object

  • 403

    An API key is refused by class (credential_not_accepted); an OIDC caller below admin gets insufficient_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 (tenant, route, key); a REPLAY (same key + byte-identical body, after the first completed) returns the recorded response verbatim WITHOUT re-executing — so a network-blip retry never creates a duplicate customer/subscription/key. A key reused with a DIFFERENT body is 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY; a key whose first request is still in flight is 409 REQUEST_IN_PROGRESS (retryable). Keys expire after 24h — a key reused after expiry is a fresh request, not a replay. Transparent when omitted (the request runs normally).

Länge: max. 255

Request-Body erforderlich

application/json CreateInboundSource

Antworten

  • 201

    Created. Body is the source plus (for signed types) the full plaintext secret.

    application/json InboundSourceWithSecret

  • 400

    Validation error, mode_mismatch, entity_mismatch, or ssrf_blocked.

  • 403

    An API key is refused by class (credential_not_accepted); an OIDC caller below admin gets insufficient_role.

  • 409

    Idempotency-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 in resource without the secret. Do not retry it.

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

  • 200

    The source (masked configPrefix, never the plaintext).

    application/json InboundSource

  • 404

    Not 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

  • 200

    The updated source.

    application/json InboundSource

  • 400

    Validation error (incl. an attempt to change an immutable field).

  • 404

    Not 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

  • 204

    Disabled.

  • 404

    Not 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

  • 200

    The full plaintext verification config.

    application/json object

  • 404

    Not 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 (tenant, route, key); a REPLAY (same key + byte-identical body, after the first completed) returns the recorded response verbatim WITHOUT re-executing — so a network-blip retry never creates a duplicate customer/subscription/key. A key reused with a DIFFERENT body is 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY; a key whose first request is still in flight is 409 REQUEST_IN_PROGRESS (retryable). Keys expire after 24h — a key reused after expiry is a fresh request, not a replay. Transparent when omitted (the request runs normally).

Länge: max. 255

Antworten

  • 200

    The source plus the new full plaintext secret.

    application/json InboundSourceWithSecret

  • 404

    Not found / not visible.

  • 409

    This verification method has no rotatable secret (no_secret_to_rotate); or an Idempotency-Key conflict — 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) or idempotency_replay_secret_withheld (the rotation with this key already succeeded; resource is the source, without the secret).

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

  • 200

    The updated source, carrying the resolved autoReconcile state.

    application/json InboundSource

  • 400

    Validation error — an unknown property, or a non-boolean enabled.

    application/json ValidationError

  • 403

    An API key is refused by class (credential_not_accepted); an OIDC caller below admin gets insufficient_role.

  • 404

    Not found / not visible.

  • 422

    auto_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. details names sourceCreatedAt, grantedAt and currentDisclosureVersion. NOTHING WAS WRITTEN. Confirm the authorisation again on PUT /settings/auto-reconcile.

    application/json ValidationError

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

  • 202

    The synthetic event ran the pipeline.

    application/json object

  • 400

    Validation error, or unsupported_method (an edge-verified source).

  • 403

    An API key is refused by class (credential_not_accepted); an OIDC caller below admin gets insufficient_role.

  • 404

    Not found / not visible / disabled source.

  • 409

    Not 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. (hmac sources).

Webhook-Signature Header string

v1,<base64> — HMAC-SHA256 over {Webhook-Id}.{Webhook-Timestamp}.{raw body}. May carry several space-delimited signatures during a secret rotation; accept if ANY verifies. (hmac sources).

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). (hmac sources).

X-Inbound-Token Header string

The shared token we assigned (shared_secret sources).

Request-Body erforderlich

application/json object

Antworten

  • 200

    Verified + logged (processed/ignored), a deduped redelivery, or a stale-but-signed replay — the sender must stop retrying. Body is metadata-only ({ outcome, eventId? }).

  • 401

    Missing/invalid signature — rejected before any side-effect, never dispatched.

  • 404

    Unknown 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 rejected_* values rather than four. Members: processed, ignored_duplicate, ignored_already_paid, rejected_signature, rejected_no_match, rejected_stale, rejected_unparseable, pending_confirmation. An unknown member is a clean 400, never an empty page. ⛔ Deliberately type: string and NOT an enum — an enum here would document the comma form as invalid, which is the one form this parameter exists to accept.

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

limit Query integer
Standard: 50
Bereich: ≥ 1, ≤ 100

Antworten

  • 200

    A page of received events (metadata-only) + an opaque nextCursor.

    application/json object

  • 400

    The query string was refused. Three distinguishable facts share this status, and a client should branch on the error code rather than on the status: unknown_query_parameter — the request named one or more query parameters this deployment does not define, and parameters lists 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 answered 200 OK with 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/json UnknownQueryParameterError | ValidationError

  • 403

    Caller is not an active member (insufficient_role), or an api key lacks payments:read. ⛔ NOT an admin bar: reads are open to readonly and 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

  • 200

    The event metadata + the derived step timeline.

    application/json InboundEventDetail

  • 404

    Not 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

  • 202

    The replay ran; the terminal outcome is returned inline.

    application/json object

  • 403

    Caller lacks the admin role.

  • 404

    Not 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 pending_confirmation event the matching engine suggested).

Antworten

  • 200

    Confirmed (or an idempotent replay of an already-processed event). Body is metadata-only: { outcome: 'processed', invoiceId, paymentState?, statusFlipped?, emitted? }.

    application/json object

  • 403

    Insufficient role (admin/owner required).

  • 404

    No such event in the caller's entity/mode scope.

  • 409

    Not confirmable (not_confirmable) — the event is not in pending_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 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_BODY; one still in flight is 409 REQUEST_IN_PROGRESS. Keys expire after 24h. A 4xx consumes the key — a corrected retry needs a fresh one.

Länge: max. 255

Request-Body erforderlich

application/json object

Antworten

  • 201

    The recorded payment and the invoice's resulting state (or, on an Idempotency-Key replay, the recorded response).

    application/json object

  • 400

    payment_intent_required (no body / unparseable / mode absent — 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_remaining carrying amountMinor/allowOverpayment — details.conflictingFields), payment_amount_required (exact_amount whose amountMinor is absent, a string, fractional or out of safe-integer range — details.field), or idempotency_key_required.

    application/json PaymentIntentError

  • 403

    Caller lacks the payments:write scope (a readonly member 403s here).

  • 404

    Invoice not in scope, or manual payment entry is not enabled.

  • 409

    invoice_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 (an exact_amount above the invoice's open amount with no allowOverpayment: true — details.openAmountMinor states the true remaining balance). Also IDEMPOTENCY_* 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 then gross − already allocated − the same-currency partial credit notes. That is the figure settle_remaining books and the figure an exact_amount is measured against.

  • 422

    payment_amount_out_of_range (an amountMinor of 0 or below — read and understood, and not a receipt; to take a recorded payment back, reverse its allocation), or validation_error for a valueDate outside [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

  • 200

    The ledger projection (a 200 also tells a client the surface is enabled).

    application/json InvoicePaymentLedger

  • 403

    Caller lacks the payments:read scope.

  • 404

    Invoice 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 GET /invoices/{id}/payments.

Idempotency-Key erforderlich Header string

REQUIRED on this route — same contract as POST /invoices/{id}/payments.

Länge: max. 255

Request-Body erforderlich

application/json object

Antworten

  • 201

    The compensating entry and the invoice's resulting state.

    application/json object

  • 400

    validation_error (a missing/invalid reason) or idempotency_key_required.

  • 403

    Caller lacks the payments:write scope.

  • 404

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

  • 409

    allocation_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) or allocation_is_a_reversal (a reversal is not itself reversible). Also IDEMPOTENCY_* 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 (insec_a1b2c3d4).

subscribedTypes erforderlich Array<string>
publicSlug erforderlich string

The opaque, stable /inbound/<slug> id (copyable, not reveal-once).

inboundPath veraltet string

The api-side receiver route /inbound/<slug>, for callers inside the platform's network only. ⛔ Not a public address — it carries no host and no /v1; use inboundUrl.

inboundUrl string | null

The COMPLETE receiving address a sender is configured with, e.g. https://api.fortlauf.de/v1/inbound/<slug>. Use it verbatim — never compose an address from a host and inboundPath. null while this environment does not yet accept notifications from outside senders.

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; null means nothing ever has, which is the only way to answer „is this source alive?" without opening the event log. ⚠ A whole-tenant source (entityId: null) reports null forever by construction — the receiver refuses one — which is correct rather than a defect.

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 fetch_to_confirm; SSRF-checked at creation (internal host ⇒ 400).

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 (insec_a1b2c3d4).

subscribedTypes erforderlich Array<string>
publicSlug erforderlich string

The opaque, stable /inbound/<slug> id (copyable, not reveal-once).

inboundPath veraltet string

The api-side receiver route /inbound/<slug>, for callers inside the platform's network only. ⛔ Not a public address — it carries no host and no /v1; use inboundUrl.

inboundUrl string | null

The COMPLETE receiving address a sender is configured with, e.g. https://api.fortlauf.de/v1/inbound/<slug>. Use it verbatim — never compose an address from a host and inboundPath. null while this environment does not yet accept notifications from outside senders.

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; null means nothing ever has, which is the only way to answer „is this source alive?" without opening the event log. ⚠ A whole-tenant source (entityId: null) reports null forever by construction — the receiver refuses one — which is correct rather than a defect.

secret string

The full plaintext verification secret (present only for hmac/shared_secret).

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 idempotency_replay_secret_withheld: the resource the first request created, as it answered then but WITHOUT the one-time secret field.

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
Feld Typ Beschreibung
path string
message string
code string

Optional machine-readable reason for the refusal, for a client that shows its own wording instead of message. Present only where an endpoint documents it; an issue without it is refused for the reason message states. Today: invalid_address (an entry of a copy list is not one valid address), too_many_addresses (more than 5 entries in one copy list), copies_require_email (copy recipients are set but email is empty), and service_timing_conflict (POST /invoices, PUT /invoices/{id}: the invoice would state both a single service date and a service period; the issue's path is deliveryDate). On the invoice saves and POST /invoices/preview-totals also reverse_charge_buyer_vat_id_required (a reverse-charge line needs the buyer's VAT identification number), vat_category_unavailable (the line's VAT category is not available), vat_rate_category_mismatch (the rate does not fit the line's VAT category), vat_exemption_reason_required / vat_exemption_reason_not_allowed (the exemption reason is missing, or not allowed for that category), and, only where the deployment enforces the check, unit_code_unknown, currency_code_unknown and vat_id_prefix_unknown (the same three also appear on quote and recurring-template saves, with the same paths). The set may grow, so treat an unknown value like an absent one. Never carries a submitted value.

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 outcome — see the schema.

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 outcome — see the schema.

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 remittanceReference, which is the extracted matching signal. Banks assemble this field from 35-character chunks joined mid-word, so the two genuinely differ and the raw form must never be normalised for display.

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 (reconciliation.state as its detail) and is APPENDED rather than substituted for „Aktion ausgeführt": the arrival's verdict is a historical fact, and a timeline that edited its own earlier step would be the same mutation the stored-outcome shape was refused for, relocated into a projection.

Felder
Feld Typ Beschreibung
step erforderlich string
status erforderlich string
Werte: "ok", "failed", "pending", "skipped"
detail erforderlich string

Metadata only — a Beleg-Nr. / status code, never a customer name or raw body.

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 — issued → paid when the invoice becomes exactly paid, and paid → issued when a reversal takes it back below full. Never written directly; only ever derived from the ledger.

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: totalGross − amountPaid − the same-currency partial credit notes. Negative when overpaid. This is the figure an exact_amount payment is measured against, and the one a payment_exceeds_open_amount refusal quotes.

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
Feld Typ Beschreibung
supported Array<string>

The modes this deployment will execute.

received beliebig

The mode value that was sent.

conflictingFields Array<string>

Fields that are not legal in the stated mode.

field string

The field that could not be read (amountMinor).

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. false for a newly registered source — adding a source never changes what books.

state erforderlich string

auto — the next matched payment from this source books itself. off — the administrator's own answer for this source. ⭐ It takes precedence over every other reason, so a surface can show which sources are switched off and which will start booking the moment the setting is switched on. needs_reaffirmation — switched on, the authorisation stands, but this source was registered after it was given. Confirm the authorisation again for the company. blocked_no_consent — nothing books from any source, whatever this switch says, because the company has no active authorisation. blocked_platform — this environment cannot book automatically at all. source_disabled — the question does not arise: a disabled source's receiving address answers 404 and records nothing.

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. false when there is no authorisation — „covered by nothing" is not true.

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

not_booked — nothing was ever allocated for this arrival (the pending_confirmation and rejected_no_match case). standing — allocated, and nothing has been reversed. partially_reversed — some of it has been compensated by a Gegenbuchung, and some still stands. fully_reversed — all of it has been compensated; the invoice is open again, back in the open items and in the Mahnwesen.

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 grossAllocatedMinor ⇒ nothing has been reversed.

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 — totalGrossMinor − amountPaidMinor − creditedMinor, where creditedMinor is the sum of the invoice's issued/paid same-currency PARTIAL credit notes — and every surface that states the balance before money is booked publishes that one derivation's output under this same name and unit, so a read, a refusal and a confirm step can never disagree. Never client-computed; and never re-derived server-side either — a local totalGrossMinor − amountPaidMinor is credit-blind and would state €1 000 where €700 will be booked.

paymentState erforderlich string
Werte: "open", "partially_paid", "paid", "overpaid"
currency string

The invoice's ISO 4217 currency code (for example EUR), the unit of every amount in this summary and in the ledger entries. Returned by GET /invoices/{id}/payments.

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 reversal row.

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

manual — recorded by a person through this API. inbound — booked from a received payment notification, whether a person confirmed it or it was booked automatically (see bookedAutomatically).

Werte: "manual", "inbound"
bookedAutomatically erforderlich boolean

⭐ True iff the AUTOMATIC inbound rail booked this entry — i.e. nobody pressed a button. It exists because origin alone is not enough: inbound covers BOTH a credit a person confirmed from the received-payments worklist AND one the platform booked unattended under the company's standing authorisation. Until this field existed the only record of the difference was the account's own activity history, which only an owner may read — so the consequence of switching automatic booking on is now readable on the document it landed on, by the same person who may reverse it. Always false on a reversal row: a reversal is always a human act, enforced by a database CHECK rather than by convention.

reversed erforderlich boolean

True on a payment row that has been reversed. Always false on a reversal row.

reversalAllocationId string (uuid)

On a reversed payment row — the entry that reverses it.

reversesAllocationId string (uuid)

On a reversal row — the entry it compensates.

reversalReason string

On a reversal row — the stated category. Present because stating a reason is MANDATORY on the reversal, and write-only data on a compliance ledger is a defect: the audit log has no detail column and is owner-only, so this is the only place the reason is readable by the person who wrote it.

Werte: "wrong_amount", "wrong_invoice", "never_received", "duplicate_entry", "other"
reversalNote string

On a reversal row that has one — the free-text note stated with the reversal, returned exactly as stored. Absent (not null) on a reversal without a note and on every payment row. Internal to the account: readable by anyone who may read this ledger, and never rendered to the customer — it does not appear in a PDF, an XML, a webhook payload or an e-mail.

Länge: max. 280

Verwendet von: GET /invoices/{id}/payments

Nach oben · Alle Ressourcengruppen · OpenAPI-Spezifikation (YAML)