Skip to content

Endpoints

The customer resource (/v1/customers) is your debtor register. Fields are English; amounts and conventions as above.

MethodPathScopeNotes
GET/v1/customerscustomers:readCursor list. Filters: cvr, email (exact match), externalReference
POST/v1/customerscustomers:writeCreate. 201 + Location
GET/v1/customers/{id}customers:read
PATCH/v1/customers/{id}customers:writePartial: omitted fields kept, null clears. Last-write-wins
GET/v1/customers/{id}/departmentscustomers:readDepartments (contact person + reference)
POST/v1/customers/{id}/departmentscustomers:write
DELETE/v1/customers/{id}/departments/{deptId}customers:writeIdempotent 204
Terminal window
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).

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.

MethodPathScopeNotes
GET/v1/productsproducts:readFull list; ?active=true filters
POST/v1/productsproducts:writesku unique per organisation (case-insensitive)
GET/v1/products/{id}products:read
PATCH/v1/products/{id}products:writePartial; "active": false deactivates
GET/v1/accountsproducts:read?active=true filters
POST/v1/accountsproducts:write
Terminal window
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 }'

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.

MethodPathScopeNotes
GET/v1/invoicesinvoices:readCursor list. Filters: status, customerId, externalReference
POST/v1/invoicesinvoices:writeCreates a draft. 201 + Location
GET/v1/invoices/{id}invoices:readHeader + lines
PUT/v1/invoices/{id}invoices:writeFull replace, drafts only (422 invoice_not_editable otherwise). Deliberately PUT: lines are always recalculated as a whole
DELETE/v1/invoices/{id}invoices:writeDrafts only
POST/v1/invoices/{id}/issueinvoices:writeOptional body { "issueDate", "dueDate" }. Returns the invoice with its number
POST/v1/invoices/{id}/sendinvoices:writeEmails the PDF to the customer; resend replays
GET/v1/invoices/{id}/pdfinvoices:readThe WORM-locked PDF (document rate class)
GET/v1/invoices/{id}/timelineinvoices:readStable event codes: created, issued, sent, delivered, opened, reminder, payment, collections_ready, collections, …
Terminal window
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 fastest integration in Danish invoicing — create the customer, the invoice, issue it (sequential number + locked PDF) and email it, in one call:

Terminal window
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) or customerId — never both. Inline customers are found-or-created (matched on externalReference, then CVR, then email) and never overwritten; inline creation also requires the customers:write scope.
  • send=true implies issue=true. Creation + issuing is atomic; sending runs after — a mail failure never rolls back an issued invoice. Check meta.send ({"status": "sent"} or {"status": "failed", "code": "customer_email_missing"}): 201 means “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.

MethodPathScopeNotes
GET/v1/invoices/{id}/paymentspayments:readThe invoice’s payments, newest first
POST/v1/invoices/{id}/paymentspayments:writeRegister a manual payment (bank transfer). Body { "amountOere", "paymentDate" }
GET/v1/paymentspayments:readOrg-wide cursor list — built for nightly reconciliation
GET/v1/payments/{id}/receiptpayments:readCreditor-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.

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

MethodPathScopeNotes
POST/v1/invoices/{id}/creditinvoices:writeBody { "reason": "…" }. 201 + the credit note; 422 invoice_not_creditable if not issued / already credited / in collections
GET/v1/creditnotesinvoices:readCursor list; ?customerId= filter
GET/v1/creditnotes/{id}invoices:readHeader + lines; creditsInvoiceId points at the original
GET/v1/creditnotes/{id}/pdfinvoices:readWORM 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.

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.

MethodPathScopeNotes
GET/v1/invoices/{id}/remindersreminders:readSent/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}/remindersreminders:writeSend the next reminder immediately. 200 + { reminderNumber, feeOere, collectionsReady }
GET/v1/reminder-settingsreminders:readEffective configuration (defaults filled in: cadence 7/14/21 days, fee at the statutory cap)
PUT/v1/reminder-settingsreminders:writeFull 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 collectionsReady is true.
  • 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 enabled letterFallback, 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).

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:

Terminal window
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.

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:

Terminal window
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.

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.

MethodPathScopeNotes
GET/v1/subscriptionssubscriptions:readCursor list. Filters: status (active/paused/cancelled), customerId, nextInvoiceBefore (YYYY-MM-DD), externalReference
POST/v1/subscriptionssubscriptions:writeCreate. 201 + Location
GET/v1/subscriptions/{id}subscriptions:readIncludes invoiceCount, periodsBehind, latestInvoice
PATCH/v1/subscriptions/{id}subscriptions:writePartial: omitted fields kept. Never touches already-issued invoices
POST/v1/subscriptions/{id}/pausesubscriptions:writeOnly from active
POST/v1/subscriptions/{id}/resumesubscriptions:writeOnly from paused
POST/v1/subscriptions/{id}/cancelsubscriptions:writeFinal — a cancelled subscription cannot be revived
GET/v1/subscriptions/{id}/invoicessubscriptions:readThe generated invoices, newest period first
GET/v1/subscriptions/{id}/next_invoicesubscriptions:readPreview of the next occurrence (date, lines, VAT, total) — nothing is issued. data: null when nothing is coming
POST/v1/subscriptions/{id}/runsubscriptions:writeIssue the next due period now. document budget
Terminal window
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:

  • status is never a writable field. Transitions are named endpoints because they are asymmetric — cancel is final; an invalid transition answers 422 invalid_transition with an explanation, never a silent no-op.
  • nextInvoiceDate cannot be set. It advances only through the run receipt (unique per period in the database) — that is what makes double-invoicing impossible.
  • /run respects 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.

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 (needs customers:write), invoice.create (needs invoices:write; issue: true issues in the same transaction; inline customer does find-or-create and also needs customers:write), payment.register (needs payments: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 customer on the invoice instead.
  • There is deliberately no send operation — a migration of historical invoices must never e-mail a thousand customers.
  • Your Idempotency-Key covers the whole batch (replay returns the stored response), and each payment.register books on key:index — retries never double-register.
  • The batch call draws from the document budget (120/min), since it may issue invoices.

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:

MethodPathNotes
POST/v1/verification-sessionsMitID 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-sessionsThe 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-connectionsAccounting 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.

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:

MethodPathScopeNotes
GET/v1/reminder-flowsreminders:readYour flows + platform standards (usable as templates)
POST/v1/reminder-flowsreminders:writeNew 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:readThe graph (?standard=true reads the platform standard as a template)
POST…/versions/{v}/approvereminders:writeFour-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}/activatereminders:writeBody { "active": true }. Only approved versions; one active version per key

Approved versions are immutable — a change is always a new version.

MethodPathScopeNotes
GET/v1/membersmembers:readActive members with role
GET/v1/rolesmembers:readThe role catalogue for your organisation type (stable keys)
PATCH/v1/members/{userId}members:manageBody { "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):

MethodPathNotes
GET/v1/brandingLogo status (light + dark variant)
PUT/v1/branding/logoBody { "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-domainYour own payment host (CNAME → pay.rieck.nu). Body { "domain": "pay.acme.example" }; null clears. Changing it resets verification
POST/v1/payment-domain/verifyRun 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-portalSubdomain, 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-domainSame 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).