Skip to content

Webhooks

Outbound webhooks (signed, with retry/backoff and secret rotation) notify you of case and invoice events so you don’t have to poll. Configuration lives under Admin → Webhooks; the signing contract follows Standard Webhooks.

Invoice event types: invoice.issued, invoice.sent, invoice.paid, invoice.overdue, invoice.credited, invoice.collections, payment.received. The envelope carries an invoice: { "id": "…", "sekvensnummer": n } block — sort by it per invoice; gaps are legal, duplicates never are (webhook-id is your dedup key). Payloads carry amounts, dates, ids and your own externalReference — never personal data.

Customer events: customer.past_due, customer.settled (see Gating services on unpaid balances), with a customer: { "id", "sekvensnummer" } block.

Subscription events, with a subscription: { "id", "sekvensnummer" } block: subscription.created / updated / paused / resumed / cancelled / ended, subscription.invoiced (a period was billed — payload carries the invoiceId), subscription.payment_succeeded / payment_failed (the stored-card charge — the event the service-gating pattern rests on: on payment_failed the invoice simply continues in the reminder flow, and customer.past_due follows if it stays unpaid), and subscription.payment_method.attached / detached / expiring.

subscription.updated carries the diff, so you can reconcile without polling: changed (the API field names that moved), priceOere and previousPriceOere (both, always — also when the price did not change, so “unchanged” never looks like “missing”), effectiveFrom (the date the change takes effect — today the next occurrence, since a recipe change bites from the next period), and intervalUnit / intervalCount with their previous* counterparts only when the interval changed.

⚠️ priceOere is the recipe’s net per occurrence, before VAT and after line discounts. It is not the invoice amount: an occurrence’s total is settled when it runs (prices that follow a price list are re-read, quantity tiers apply, and VAT is added per line). Reconcile against the issued invoice — never against the subscription. GET /v1/subscriptions/{id} answers with the current recipe and makes no claim about how it looked on any past date.

Payment lifecycle events (no invoice/customer/subscription block — the ids live in data): payment.failed (a card charge or checkout payment was declined; declineType is soft — the same card may be retried — or hard; attemptKind says which flow), refund.succeeded (the gateway returned money to the payer — refundId, amountOere, reference, invoiceId, caseId), refund.failed (failureType: "declined" is a no; "unresolved" means we got no answer and released the reservation — a later refund.succeeded may follow, so do not treat it as final), dispute.created (a chargeback was debited — disputeId, amountOere, reference, invoiceId, caseId) and dispute.closed (outcome: "upheld" the money stays gone, "reversed" it came back, "dismissed" no consequence). Reasons are always classes, never the provider’s own codes or text; settlement.created tells you a settlement was drawn up (not paid out).

Payment-plan events (no payment_plan block — planId lives in data, sequence numbers are per plan): payment_plan.created, payment_plan.activated (the customer signed — reminders on the covered invoices are paused from here), payment_plan.instalment_paid (invoiceId, amountOere, cumulative paidOere/remainingOere), payment_plan.defaulted (wasActive) and payment_plan.closed (outcome: fulfilled / cancelled). Never the customer’s link, the note or card data.

Quote events (no quote block — quoteId lives in data, sequence numbers are per quote): quote.sent (grossOere, validUntil, contentHash, resent), quote.viewed (via), quote.accepted (acceptedBy: customer through the quote link or creditor recorded in the portal — not the same evidence; signed when a signature was required), quote.declined and quote.expired. quote.accepted is the one to build on: it is the moment the work is ordered.

Order events (no order block — orderId lives in data, sequence numbers are per order): order.created, order.confirmed (contentHash of the frozen content), order.invoiced (invoiceId + status: partially_invoiced or invoiced — fired on every invoicing, so a staged project yields one per stage) and order.cancelled.

Notification events (no notification block — notificationId lives in data, sequence numbers are per notification, so always 1): notification.created (type, caseId, urgent). Only the organisation-wide ones; never the title or the portal path. This is the event that replaces “log in and check” — the list endpoint is only for catching up.

Rental events (no block of their own — tenancyId/propertyId live in data): tenancy.created, .activated, .terminated (kind, terminatedBy), .ended (moveOutDate) and property.status_changed (from, to). ⚠️ No payload carries the tenant (a person — read her with the tenancy under scope) or the legal ground for a rescission (the landlord’s characterisation of the tenant’s conduct, which belongs in the case and the letter).

Customer-check events (no block of their own — customerCheckId lives in data): customer_check.created, .status_changed, .risk_decided, .approved, .rejected, .ended. ⚠️ decidedBy appears only on .risk_decided, where the actor is the proof of human intervention, and no payload ever carries a reason or a rejection category.

E-invoice events (no block of their own — invoiceId lives in data): einvoice.delivered (recipientEndpoint, transportReference, documentHash) and einvoice.failed (stage transport | recipient, reason, responseCode). ⚠️ Nothing fires when the transport merely succeeds — a delivery is the recipient’s answer, and invoice.sent already reported the send.

Dispatch events (no dispatch block — dispatchId lives in data): dispatch.delivered and dispatch.failed (reason in plain text), each with subject, caseId/invoiceId, channel and our own reference. ⚠️ Nothing fires for a soft bounce, a queued dispatch or a skipped one, and delivered can never come from e-Boks — see deliveryProvable.

Contract events (no contract block — contractId lives in data, sequence numbers are per contract): contract.sent, contract.party_signed (signedCount of signerCount), contract.party_declined (⚠️ the contract stays sent — it will never be signed unless you act), contract.signed, contract.cancelled and contract.expired. Never the content, the parties’ contact details or the signature evidence.

Task events (no task block — taskId lives in data, sequence numbers are per task): task.created (type, approval, caseId, invoiceId, dueDate), task.completed (outcome: approved / declined / answered, and via: portal / mail / api) and task.cancelled. Never the answer itself and never the task’s title. task.created with approval: true is the one to build on: it is a decision waiting on you.

Webhooks can be lost or arrive out of order: treat them as a signal to fetch, and use GET /v1/invoices/{id} as the truth.

Everything the portal’s webhook page can do, headless (webhooks:manage):

MethodPathNotes
GET/v1/webhook-endpointsAll endpoints (max 10 per organisation) — never includes secrets
POST/v1/webhook-endpointsRegister. Response carries secret (whsec_…) exactly once — store it; lost = rotate. HTTPS only, no credentials in the URL, private/internal hosts rejected
GET/v1/webhook-endpoints/{id}
PATCH/v1/webhook-endpoints/{id}Partial: eventTypes, active, description. The URL cannot be changed — register a new endpoint instead
DELETE/v1/webhook-endpoints/{id}Final; queued deliveries disappear with it
POST/v1/webhook-endpoints/{id}/rotate-secretNew secret; the old one keeps signing for 24 h (two signatures in the header) so in-flight deliveries survive the switch
POST/v1/webhook-endpoints/{id}/testQueue a Ping with no real data — proves your endpoint and signature verification. 202

If your endpoint has been down, you never depend on support to get your data back:

MethodPathScopeNotes
GET/v1/eventsevents:readPull feed over the same event log webhooks deliver from. ?since= (ISO-8601), ?type=, cursor-paginated, ascending (oldest first). Each item has exactly the webhook delivery shape — reuse your webhook handler
POST/v1/webhook-endpoints/{id}/replaywebhooks:manageBody { "since": "…" } (at most 90 days back). Re-queues failed/given-up deliveries and enqueues events that never got queued (endpoint created later, our downtime). Delivered events are never re-sent. Response: { requeued, enqueued, possiblyMore } (cap 10,000 per call — call again if possiblyMore)

The feed is the truth-side companion to webhooks: webhooks push the signal, GET /v1/events?since= closes any gap.