Endpoints
Customers
Section titled “Customers”The customer resource (/v1/customers) is your debtor register. Fields are English;
amounts and conventions as above.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/customers | customers:read | Cursor list. Filters: cvr, email (exact match), externalReference |
| POST | /v1/customers | customers:write | Create. 201 + Location |
| GET | /v1/customers/{id} | customers:read | |
| PATCH | /v1/customers/{id} | customers:write | Partial: omitted fields kept, null clears. Last-write-wins |
| GET | /v1/customers/{id}/departments | customers:read | Departments (contact person + reference) |
| POST | /v1/customers/{id}/departments | customers:write | |
| DELETE | /v1/customers/{id}/departments/{deptId} | customers:write | Idempotent 204 |
curl -X POST https://app.rieck.nu/api/v1/customers \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: customer-crm-9001" \ -H "Content-Type: application/json" \ -d '{ "type": "company", "name": "Acme ApS", "cvr": "12345678", "email": "invoices@acme.example", "address": { "street": "Hovedgaden 1", "postalCode": "8000", "city": "Aarhus" }, "externalReference": "crm-9001", "metadata": { "segment": "b2b" } }'Customer objects carry hasCpr as a boolean only — Danish civil registration numbers can
never be read or written through this API. Email/phone can be filtered by exact match
only (they are stored encrypted with blind indexes; free-text search is deliberately
impossible).
Products & accounts
Section titled “Products & accounts”Your product catalogue (/v1/products) and sales chart of accounts (/v1/accounts).
Both share the products:* scopes. Products are deactivated, never deleted (invoice
history may reference them). A vatRateBps of 0 requires a vatExemptionReason.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/products | products:read | Full list; ?active=true filters |
| POST | /v1/products | products:write | sku unique per organisation (case-insensitive) |
| GET | /v1/products/{id} | products:read | |
| PATCH | /v1/products/{id} | products:write | Partial; "active": false deactivates |
| GET | /v1/accounts | products:read | ?active=true filters |
| POST | /v1/accounts | products:write |
curl -X POST https://app.rieck.nu/api/v1/products \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: product-ABO-12" \ -H "Content-Type: application/json" \ -d '{ "sku": "ABO-12", "name": "Subscription 12 months", "unitPriceOere": 9900, "vatRateBps": 2500 }'Invoices
Section titled “Invoices”The core resource. An invoice starts as a draft, is issued (assigned its sequential number and a hash-locked PDF, atomically — Danish bookkeeping law requires an unbroken sequence), then sent. Issued invoices are immutable.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/invoices | invoices:read | Cursor list. Filters: status, customerId, externalReference |
| POST | /v1/invoices | invoices:write | Creates a draft. 201 + Location |
| GET | /v1/invoices/{id} | invoices:read | Header + lines |
| PUT | /v1/invoices/{id} | invoices:write | Full replace, drafts only (422 invoice_not_editable otherwise). Deliberately PUT: lines are always recalculated as a whole |
| DELETE | /v1/invoices/{id} | invoices:write | Drafts only |
| POST | /v1/invoices/{id}/issue | invoices:write | Optional body { "issueDate", "dueDate" }. Returns the invoice with its number |
| POST | /v1/invoices/{id}/send | invoices:write | Emails the PDF to the customer; resend replays |
| GET | /v1/invoices/{id}/pdf | invoices:read | The WORM-locked PDF (document rate class) |
| GET | /v1/invoices/{id}/timeline | invoices:read | Stable event codes: created, issued, sent, delivered, opened, reminder, payment, collections_ready, collections, … |
curl -X POST https://app.rieck.nu/api/v1/invoices \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: invoice-order-4711" \ -H "Content-Type: application/json" \ -d '{ "customerId": "<customer uuid>", "lines": [ { "description": "Consulting", "quantity": 10, "unitPriceOere": 95000, "vatRateBps": 2500 } ], "paymentTermsDays": 14, "externalReference": "order-4711", "metadata": { "shopOrderId": "4711" } }'The one-call shortcut
Section titled “The one-call shortcut”The fastest integration in Danish invoicing — create the customer, the invoice, issue it (sequential number + locked PDF) and email it, in one call:
curl -X POST "https://app.rieck.nu/api/v1/invoices?issue=true&send=true" \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: order-4712" \ -H "Content-Type: application/json" \ -d '{ "customer": { "type": "company", "name": "Acme ApS", "cvr": "12345678", "email": "invoices@acme.example", "externalReference": "crm-9001" }, "lines": [ { "description": "Order 4712", "quantity": 1, "unitPriceOere": 80000 } ], "externalReference": "order-4712" }'- Pass
customer(inline) orcustomerId— never both. Inline customers are found-or-created (matched onexternalReference, then CVR, then email) and never overwritten; inline creation also requires thecustomers:writescope. send=trueimpliesissue=true. Creation + issuing is atomic; sending runs after — a mail failure never rolls back an issued invoice. Checkmeta.send({"status": "sent"}or{"status": "failed", "code": "customer_email_missing"}):201means “the invoice exists”, not “the email arrived”.- The query flags are part of the idempotency hash: replaying the same key with different
flags returns
409 idempotency_conflict, never the wrong response. - Everything else — reminders per your settings, and collections via
POST /v1/invoices/{id}/collect(coming) — happens on our side. Subscribe to webhooks for the rest of the story.
Amount fields on responses: netOere, vatOere, grossOere, paidOere, creditedOere,
reminderFeeOere and balanceOere — what the customer owes right now
(gross + reminder fees − paid − credited), always computed, never stale.
Collections — the bridge no one else has
Section titled “Collections — the bridge no one else has”POST /v1/invoices/{id}/collect (scope: cases:write)Hands an unpaid, overdue invoice over to debt collection — same engine, same compliance
gates as everything else (Danish 10-day demand letter, fee caps, the works). The invoice
must be issued, past due and not fully paid. 201 + Location: /v1/cases/{caseId};
calling again returns 200 with the same case ("replayed": true). Rejections are
specific: invoice_not_due, invoice_already_paid, reminder_flow_incomplete,
only_fees_outstanding.
From there, GET /v1/cases/{caseId} and the sag.* webhook events tell the story.
Payments
Section titled “Payments”| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/invoices/{id}/payments | payments:read | The invoice’s payments, newest first |
| POST | /v1/invoices/{id}/payments | payments:write | Register a manual payment (bank transfer). Body { "amountOere", "paymentDate" } |
| GET | /v1/payments | payments:read | Org-wide cursor list — built for nightly reconciliation |
| GET | /v1/payments/{id}/receipt | payments:read | Creditor-branded receipt PDF (generated moments after registration — a 404 right away means retry shortly) |
Your Idempotency-Key is the bookkeeping key: the same key never registers a payment
twice, no matter when the retry arrives, and replays return the original response. The
response reports meta.outcome (partial / paid_in_full / registered) and the
updated invoice with its balanceOere.
Card/MobilePay payments arrive by themselves (via payment links and the coming embedded
checkout) and show up in the same lists with "source": "altapay" / "mobilepay" — plus
a payment.received webhook either way.
Payment links
Section titled “Payment links”POST/GET /v1/invoices/{id}/payment-link (payments:write / payments:read)Mints the invoice’s stable payment link (idempotent — POST and GET return the same
link). The response carries url (honours your verified custom payment domain), the
128-bit reference and the short typeable paymentCode (for pay.rieck.nu). The checkout
session itself is created when the customer clicks — always for the current balance,
so a link sent before a partial payment collects the remainder. Card data never touches
your systems. Drafts have no link (422 invoice_not_issued).
Hosted checkout sessions for your own UI (POST /v1/checkout-sessions) and the embedded
payment window arrive with the AltaPay production cutover.
Credit notes
Section titled “Credit notes”Credit notes share the invoice table and number series ("type": "creditNote") but have
their own resource path. Crediting is full, once per invoice, and requires a reason
(Danish bookkeeping law — the trail must explain why). The credit note is issued and
PDF-locked in the same call; the original invoice’s creditedOere and balanceOere
reflect it immediately.
| Method | Path | Scope | Notes |
|---|---|---|---|
| POST | /v1/invoices/{id}/credit | invoices:write | Body { "reason": "…" }. 201 + the credit note; 422 invoice_not_creditable if not issued / already credited / in collections |
| GET | /v1/creditnotes | invoices:read | Cursor list; ?customerId= filter |
| GET | /v1/creditnotes/{id} | invoices:read | Header + lines; creditsInvoiceId points at the original |
| GET | /v1/creditnotes/{id}/pdf | invoices:read | WORM PDF (document class) |
Note on status: overdue reflects the reminder engine having acted, not the calendar.
To gate services on “does this customer owe money”, use balanceOere (and the balance
endpoints arriving with the past-due milestone) — not status.
Reminders (dunning)
Section titled “Reminders (dunning)”Rieck runs the reminder engine for you on the cadence you configure — most integrations never call these endpoints. They exist for two things: reading the reminder history, and sending the next reminder now, outside the cadence.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/invoices/{id}/reminders | reminders:read | Sent/failed reminders (type: reminder / payment_reminder) with feeOere and sentAt; meta.maxReminders + meta.nextReminder (the engine’s next planned reminder and date, or null) |
| POST | /v1/invoices/{id}/reminders | reminders:write | Send the next reminder immediately. 200 + { reminderNumber, feeOere, collectionsReady } |
| GET | /v1/reminder-settings | reminders:read | Effective configuration (defaults filled in: cadence 7/14/21 days, fee at the statutory cap) |
| PUT | /v1/reminder-settings | reminders:write | Full replacement of the configuration (settings + cadence written atomically) |
The ad-hoc POST skips the cadence plan but never the Danish §9b floor, which is enforced in the database no matter who asks:
- The invoice must actually be overdue with a positive balance — otherwise
422 reminder_not_eligible. - At most 3 reminders per invoice, ever. After the last one
collectionsReadyistrue. - The reminder fee is only charged when the statutory interval since the previous
reminder has passed. Send sooner and the reminder still goes out — with
feeOere: 0. The floor cannot be bypassed; only the fee lapses. - A customer without an e-mail address yields
422 customer_email_missing(unless you enabledletterFallback, in which case a physical letter is sent instead).
PUT /v1/reminder-settings configures aggressiveness within the floor: fewer
reminders (maxReminders 0–3), a lower fee (feeOere, null = statutory cap, currently
10000 øre), a tighter or looser cadence (strictly increasing days after due date), the
free-of-charge paymentReminder before the fee-bearing reminders, letterFallback, and
autoCollections (hand the invoice to debt collection automatically after the final
reminder — the bridge described under invoices).
GET /v1 — discovery
Section titled “GET /v1 — discovery”Requires api:discovery. Returns API version, your organisation id and the key’s scopes.
POST /v1/cases — create a collection case
Section titled “POST /v1/cases — create a collection case”Requires cases:write and an Idempotency-Key header. Strict JSON body, max 64 KiB:
curl -X POST https://app.rieck.nu/api/v1/cases \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: order-4711" \ -H "Content-Type: application/json" \ -d '{ "externalReference": "order-4711", "fordringer": [ { "hovedstolOere": 125000, "forfaldsdato": "2026-06-01" } ], "skyldnere": [ { "skyldnerType": "virksomhed", "navn": "Eksempel ApS", "cvr": "12345678" } ] }'201 on creation, 200 + meta.replayed: true on a safe replay. The response carries the
Rieck case id, case number, your externalReference, status and timestamps; Location
points at the status route.
The case is created in OPRETTET (created) state — the API never bypasses document
control, activation gates or compliance. Danish collection law (10-day demand letter,
fee caps, limitation periods) is enforced by the same engine the portal uses.
GET /v1/cases/{caseId} — case status
Section titled “GET /v1/cases/{caseId} — case status”Requires cases:read. Returns PII-free, event-sourced status: state, cross-cutting
status, event version and UTC timestamps. Unknown ids and other tenants’ ids both return
the same neutral 404 case_not_found.
Poll this — or better, subscribe to webhooks (below) and use polling only to reconcile.
POST /v1/betingelse-accept — terms-acceptance evidence
Section titled “POST /v1/betingelse-accept — terms-acceptance evidence”Requires terms:accept. Call it at the moment your customer accepts your terms
of sale; the record becomes admissible evidence if the claim is later disputed. Max 32 KiB:
curl -X POST https://app.rieck.nu/api/v1/betingelse-accept \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: order-4711-terms" \ -H "Content-Type: application/json" \ -d '{ "eksternOrdreRef": "order-4711", "betingelseVersion": "2026-01-01", "betingelseHash": "<sha256-hex of the exact accepted terms text>", "accepteretKl": "2026-07-27T09:00:00+00:00", "idempotensnoegle": "order-4711-terms" }'Provide fakturaId (if you know the Rieck invoice) or eksternOrdreRef (your own
order reference — it is linked when the invoice/case is created later). betingelseHash
must be the SHA-256 of the exact terms text the customer saw — that is what makes the
evidence strong. 201 for a new record, 200 for an idempotent duplicate. The body’s
idempotensnoegle deduplicates the evidence record itself; simplest is to reuse the
same value as the Idempotency-Key header.
Subscriptions
Section titled “Subscriptions”Recurring invoicing (/v1/subscriptions): the subscription is a recipe; every
occurrence is an ordinary invoice in the same unbroken number series, with the same
hash-locked PDF, the same reminder flow and — because Rieck also owns collections — a
place for a failed renewal to end. The engine issues one period per run, never a
surprise pile.
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/subscriptions | subscriptions:read | Cursor list. Filters: status (active/paused/cancelled), customerId, nextInvoiceBefore (YYYY-MM-DD), externalReference |
| POST | /v1/subscriptions | subscriptions:write | Create. 201 + Location |
| GET | /v1/subscriptions/{id} | subscriptions:read | Includes invoiceCount, periodsBehind, latestInvoice |
| PATCH | /v1/subscriptions/{id} | subscriptions:write | Partial: omitted fields kept. Never touches already-issued invoices |
| POST | /v1/subscriptions/{id}/pause | subscriptions:write | Only from active |
| POST | /v1/subscriptions/{id}/resume | subscriptions:write | Only from paused |
| POST | /v1/subscriptions/{id}/cancel | subscriptions:write | Final — a cancelled subscription cannot be revived |
| GET | /v1/subscriptions/{id}/invoices | subscriptions:read | The generated invoices, newest period first |
| GET | /v1/subscriptions/{id}/next_invoice | subscriptions:read | Preview of the next occurrence (date, lines, VAT, total) — nothing is issued. data: null when nothing is coming |
| POST | /v1/subscriptions/{id}/run | subscriptions:write | Issue the next due period now. document budget |
curl -X POST https://app.rieck.nu/api/v1/subscriptions \ -H "Authorization: Bearer $RIECK_API_KEY" \ -H "Idempotency-Key: sub-crm-77" \ -H "Content-Type: application/json" \ -d '{ "customerId": "…", "name": "Hosting monthly", "interval": { "unit": "month", "count": 1 }, "startDate": "2026-08-01", "dueDays": 14, "autoSend": true, "lines": [{ "description": "Hosting", "quantity": 1, "unitPriceOere": 100000 }] }'The contract’s guard-rails, all enforced server-side:
statusis never a writable field. Transitions are named endpoints because they are asymmetric —cancelis final; an invalid transition answers422 invalid_transitionwith an explanation, never a silent no-op.nextInvoiceDatecannot be set. It advances only through the run receipt (unique per period in the database) — that is what makes double-invoicing impossible./runrespects the engine’s lease and run receipt. Two calls for the same period produce one invoice; a future period cannot be run early (422 subscription_not_due).- Card data is never a write path — no PAN, token or agreement id anywhere in this resource. (A headless enrolment path for stored cards is on the roadmap.)
- Changing the recipe (
PATCH) affects future occurrences only; issued invoices are immutable bookkeeping records.
Subscriptions carry externalReference and metadata with the same contract as customers
and invoices (see External references & metadata): mirror your own subscription id from
day one, filter with ?externalReference=, duplicates answer 422 external_reference_in_use, and portal edits never silently clear your keys.
POST /v1/batch — bulk operations
Section titled “POST /v1/batch — bulk operations”Built for migration: up to 100 operations in one call (importing 5,000 invoices as single calls against the 300/min write budget takes three hours; as batches it takes minutes). Operations run sequentially, each in its own transaction — partial success is reported per operation and the batch never rolls back as a whole.
POST /v1/batch{ "operations": [ { "op": "customer.create", "params": { "type": "company", "name": "Acme ApS", "cvr": "12345678" } }, { "op": "invoice.create", "params": { "customer": { "type": "company", "name": "Acme ApS", "cvr": "12345678" }, "issue": true, "lines": [{ "description": "Migrated invoice", "quantity": 1, "unitPriceOere": 80000, "vatRateBasisPoints": 2500 }] } }, { "op": "payment.register", "params": { "invoiceId": "…", "amountOere": 100000, "paymentDate": "2026-01-15" } } ]}Response: 200 with data.results[] (one entry per operation: index, op, status,
and data or error in exactly the shape the single endpoint would have returned) plus
meta: { total, succeeded, failed }.
Rules worth knowing:
- Ops:
customer.create(needscustomers:write),invoice.create(needsinvoices:write;issue: trueissues in the same transaction; inlinecustomerdoes find-or-create and also needscustomers:write),payment.register(needspayments:write). Scopes are checked per operation — a missing scope fails that operation (403), never the whole batch. - Operations cannot reference each other’s results within one batch (ids are assigned at
execution). For customer-then-invoice, use the inline
customeron the invoice instead. - There is deliberately no send operation — a migration of historical invoices must never e-mail a thousand customers.
- Your
Idempotency-Keycovers the whole batch (replay returns the stored response), and eachpayment.registerbooks onkey:index— retries never double-register. - The batch call draws from the
documentbudget (120/min), since it may issue invoices.
Start links — things that need a human
Section titled “Start links — things that need a human”Four things can never become pure API calls. The API starts them and returns a link a
human opens in a browser (portal login first) — it never pretends to complete them. Call
the same endpoint again to read the outcome; all need settings:write:
| Method | Path | Notes |
|---|---|---|
| POST | /v1/verification-sessions | MitID Erhverv verification (the payout gate). Response: { url, status: "pending"|"verified", verifiedAt, cvr, payoutGateActive } — url is null once verified. 503 verification_unavailable if the broker is not configured |
| POST | /v1/card-setup-sessions | The card paying Rieck’s platform subscription. Card data goes through hosted fields in the portal only — no PAN or token ever passes this API. Response: { url, hasActiveCard } |
| POST | /v1/accounting-connections | Accounting OAuth (Dinero/e-conomic/Billy/Uniconta) — the human approves access at the provider. Response: { url, connection } |
The fourth — accepting terms on behalf of a creditor — is a legal question (it must bind a physical person unless the partner holds an explicit power of attorney) and is not exposed. Bank account changes are deliberately not startable via API at all.
Reminder flows (visual dunning graphs)
Section titled “Reminder flows (visual dunning graphs)”Creditors on the rykkerflow plan can manage their own pre-collections dunning graphs
(M23) headless — same package gate as the portal (403 rykkerflow_not_included without
it), same §9b validation, same four-eyes approval:
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/reminder-flows | reminders:read | Your flows + platform standards (usable as templates) |
| POST | /v1/reminder-flows | reminders:write | New draft version ({ key, name, graph }). The graph is the same machine format the portal builder produces; §9b/pre-collections validation runs before saving (422 flow_validation_failed with stable rule identifiers) |
| GET | /v1/reminder-flows/{key}/versions/{v} | reminders:read | The graph (?standard=true reads the platform standard as a template) |
| POST | …/versions/{v}/approve | reminders:write | Four-eyes: the API key that created the draft can never approve it — a different key or a human must (422 four_eyes_required) |
| POST | …/versions/{v}/activate | reminders:write | Body { "active": true }. Only approved versions; one active version per key |
Approved versions are immutable — a change is always a new version.
Members & roles
Section titled “Members & roles”| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/members | members:read | Active members with role |
| GET | /v1/roles | members:read | The role catalogue for your organisation type (stable keys) |
| PATCH | /v1/members/{userId} | members:manage | Body { "role": "…" }. The last admin is protected (422 last_admin_protected) |
members:manage is a privilege-escalation surface (a key can make a member admin) —
grant it only to integrations that genuinely administer users. Inviting and offboarding
users deliberately remain portal actions.
Branding, payment domain & customer portal
Section titled “Branding, payment domain & customer portal”Headless administration of what the portal’s branding page does (settings:read /
settings:write):
| Method | Path | Notes |
|---|---|---|
| GET | /v1/branding | Logo status (light + dark variant) |
| PUT | /v1/branding/logo | Body { "data": "<base64>", "mime": "image/png", "variant": "light"|"dark" }. PNG/JPEG/SVG/WebP, max 512 KB. Used on invoices, letters and the customer portal |
| DELETE | /v1/branding/logo?variant= | Remove a variant |
| GET / PUT | /v1/payment-domain | Your own payment host (CNAME → pay.rieck.nu). Body { "domain": "pay.acme.example" }; null clears. Changing it resets verification |
| POST | /v1/payment-domain/verify | Run the live check (DNS → public IP → HTTPS health). Response status: verified / dns_missing / wrong_target / not_reachable_yet / invalid. Payment links only use the domain once verified |
| GET / PATCH | /v1/customer-portal | Subdomain, active, subscriptionSelfService, primaryColor (#rrggbb), palette (identifier, e.g. lys/moerk/midnat), customDomain. PATCH is partial; the login image is portal-managed and never touched |
| POST | /v1/customer-portal/verify-domain | Same live check for the portal’s custom domain |
Domain errors are stable codes: domain_taken, subdomain_taken, subdomain_reserved,
invalid_domain, no_domain_configured.
GET /v1/settings — organisation settings
Section titled “GET /v1/settings — organisation settings”Everything an integration should read instead of guessing (settings:read):
{ "data": { "invoiceNumbering": { "mode": "auto", "startNumber": null, "seriesStarted": true }, "tone": "neutral", "chartOfAccountsSource": "rieck", "paymentDomain": { "domain": "pay.acme.example", "verified": true }, "paymentMethods": { "card": true, "mobilepay": true, "alternatives": ["klarna"] } }}tone is the wording profile on invoices/reminders (friendly / neutral / firm /
custom); chartOfAccountsSource tells you whether invoice lines post to Rieck’s chart
of accounts or your connected accounting system; paymentMethods.alternatives lists the
consumer options (Klarna/ViaBill) available on your invoices — never on collections.
Read-only: numbering and tone are configured in the portal.
GET /v1/settlements — settlement history
Section titled “GET /v1/settlements — settlement history”Read-only list of what has been paid out to you (settlements:read), newest first,
covering both families:
"type": "collections"— debt-collection settlements: recovered funds minus Rieck’s fee and offset VAT (payoutOere,feeOere,vatOere,vatOffsetOere,lineCount)."type": "invoicing"— invoice-service settlements: customer payments collected on your invoices, minus the service fee.
The endpoint reports history; it makes no promise about when the next settlement runs. Line-level detail lives in the portal’s payout pages (and their PDF specifications).