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.
Managing endpoints via API
Section titled “Managing endpoints via API”Everything the portal’s webhook page can do, headless (webhooks:manage):
| Method | Path | Notes |
|---|---|---|
| GET | /v1/webhook-endpoints | All endpoints (max 10 per organisation) — never includes secrets |
| POST | /v1/webhook-endpoints | Register. 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-secret | New 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}/test | Queue a Ping with no real data — proves your endpoint and signature verification. 202 |
Catch-up and replay
Section titled “Catch-up and replay”If your endpoint has been down, you never depend on support to get your data back:
| Method | Path | Scope | Notes |
|---|---|---|---|
| GET | /v1/events | events:read | Pull 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}/replay | webhooks:manage | Body { "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.