# Webhook events

> Every webhook Rivr sends: when it comes, which fields the body has and their types, whether it is signed and which guide shows it. The webhook is the truth about an order, but verify the signature and move the journal only when the order's status has actually changed.

- As JSON: https://developers.rivr.io/events.json
- Page in the portal: https://developers.rivr.io/events

## The delivery paths

The same order event can reach you by several paths. The body has the same shape on all of them, but the signing and which orders come differ.

### The order's callbackUrl

- Where it goes: `callbackUrl` on the order, for every order that was created with one.
- Signing: Signed with `Rivr-Signature` when the clinic or its platform has an active signing key, with one `v1` per key. Without a key the delivery comes unsigned. `Rivr-Webhook-Id` always comes along.
- Headers: `Rivr-Signature`, `Rivr-Webhook-Id`
- Guides: [Receive webhooks](https://developers.rivr.io/guides/webhooks.md), [Verify the webhook signature](https://developers.rivr.io/guides/verify-signatures.md)

### The clinic's webhook address

- Where it goes: The address configured for the clinic or its platform, for an order without a `callbackUrl`. With no address, nothing is sent this way. An order that an organisation above the clinic created with its own API key never goes here: it has the organisation's endpoints.
- Signing: Signed with `Rivr-Signature` when the clinic or its platform has an active signing key, with one `v1` per key. Without a key the delivery comes unsigned. `Rivr-Webhook-Id` always comes along.
- Headers: `Rivr-Signature`, `Rivr-Webhook-Id`
- Guides: [Receive webhooks](https://developers.rivr.io/guides/webhooks.md), [Verify the webhook signature](https://developers.rivr.io/guides/verify-signatures.md)

### Your organisation's endpoints

- Where it goes: Endpoints you add under Webhooks in the portal. They get the events of orders your organisation's own API keys create, for the clinics and events the endpoint is set to. If the order also has a `callbackUrl`, the event comes both ways.
- Signing: Always signed with `Rivr-Signature`, with one `v1` per active key on the endpoint. `Rivr-Webhook-Id` comes along. A test delivery (Send test under Webhooks in the portal) also has the header `Rivr-Webhook-Test: true` and `"test": true` in the body, with invented ids.
- Headers: `Rivr-Signature`, `Rivr-Webhook-Id`, `Rivr-Webhook-Test`
- Guides: [Receive webhooks](https://developers.rivr.io/guides/webhooks.md), [Verify the webhook signature](https://developers.rivr.io/guides/verify-signatures.md)

### The platform's onboarding address

- Where it goes: An address of your platform's that Rivr sets up on request, for onboardings the platform started with `POST /api/public/onboarding-links`.
- Signing: Signed with `Rivr-Signature` when the clinic or its platform has an active signing key, with one `v1` per key. Without a key the delivery comes unsigned. `Rivr-Webhook-Id` always comes along.
- Headers: `Rivr-Signature`, `Rivr-Webhook-Id`
- Guides: [Connect a clinic from your system](https://developers.rivr.io/guides/onboarding-links.md)

## The body

Every webhook is a `POST` with JSON. The envelope is the same for every event; `data` grows with the event.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string (uuid) | The order's id, the one you chose when you created the order. For `MerchantOnboarding`, the onboarding's id. |
| `type` | string | `Order`, or `Invoice` for an order created as a direct invoice. `MerchantOnboarding` for the onboarding notification. |
| `merchantId` | string (uuid) | The clinic the order or onboarding belongs to. |
| `status` | string | The event, see each event below. |
| `data` | object | The details. The keys are PascalCase; read them case-insensitively. New keys can be added without notice. |
| `test` | boolean | Only on a test delivery to an organisation's endpoint, and then `true`. Absent on real events. |

## The events

### The order is created, or open again

- status: `Created`
- type: `Order`, `Invoice`
- Paths: The order's callbackUrl · The clinic's webhook address · Your organisation's endpoints
- Guides: [Payment at the clinic](https://developers.rivr.io/guides/pay-at-clinic.md), [Payment on the phone](https://developers.rivr.io/guides/pay-by-link.md), [Test in the sandbox](https://developers.rivr.io/guides/testing.md)

**When it is sent.** When the order is created. Also when a cancelled or refunded order is reopened, and when a new `PUT` rewrites an existing order that isn't paid: the order is then `Created` again. A paid order is not rewritten by a `PUT` and sends no webhook.

**The fields in data**

| Field | Type | Description |
| --- | --- | --- |
| `CreatedDate` | string (date-time) | When the order was created. |
| `Reference` | string \| null | Your reference from the order. |
| `Metadata` | object (string → string) | `metadata` from the order, as you sent it. |

**Example**

```json
{
  "id": "7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14",
  "type": "Order",
  "merchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844",
  "status": "Created",
  "data": {
    "CreatedDate": "2026-09-17T09:12:03Z",
    "Reference": "visit-48213",
    "Metadata": { "bookingId": "48213" }
  }
}
```

### The payment completes later

- status: `Pending`
- type: `Order`, `Invoice`
- Paths: The order's callbackUrl · The clinic's webhook address · Your organisation's endpoints
- Guides: [Verify the webhook signature](https://developers.rivr.io/guides/verify-signatures.md), [When the patient doesn't pay right away](https://developers.rivr.io/guides/unpaid.md), [Test in the sandbox](https://developers.rivr.io/guides/testing.md)

**When it is sent.** When the patient has chosen a payment method that completes later, for example an invoice with recourse or financing that hasn't been paid out yet. `Completed` comes when the payment is done.

**The fields in data**

| Field | Type | Description |
| --- | --- | --- |
| `CreatedDate` | string (date-time) | When the order was created. |
| `Reference` | string \| null | Your reference from the order. |
| `Metadata` | object (string → string) | `metadata` from the order, as you sent it. |

**Example**

```json
{
  "id": "7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14",
  "type": "Order",
  "merchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844",
  "status": "Pending",
  "data": {
    "CreatedDate": "2026-09-17T09:12:03Z",
    "Reference": "visit-48213",
    "Metadata": { "bookingId": "48213" }
  }
}
```

### The order is paid

- status: `Completed`
- type: `Order`, `Invoice`
- Paths: The order's callbackUrl · The clinic's webhook address · Your organisation's endpoints
- Guides: [Receive webhooks](https://developers.rivr.io/guides/webhooks.md), [Verify the webhook signature](https://developers.rivr.io/guides/verify-signatures.md), [Payment at the clinic](https://developers.rivr.io/guides/pay-at-clinic.md), [Payment on the phone](https://developers.rivr.io/guides/pay-by-link.md), [When the patient doesn't pay right away](https://developers.rivr.io/guides/unpaid.md), [Bookkeeping and payouts](https://developers.rivr.io/guides/bookkeeping.md), [Payment in advance](https://developers.rivr.io/guides/prepayment.md), [Test in the sandbox](https://developers.rivr.io/guides/testing.md)

**When it is sent.** When the order is paid. Also when a claim on the order has been sold: then `PaymentMethod` is `Invoice`. An order that is reopened and paid again sends `Completed` once more.

**The fields in data**

| Field | Type | Description |
| --- | --- | --- |
| `CreatedDate` | string (date-time) | When the order was created. |
| `Reference` | string \| null | Your reference from the order. |
| `CompletedDate` | string (date-time) \| null | When the order was paid. |
| `PaymentMethod` | string | How the order was paid, grouped: `Card` (cards and digital wallets), `Swish` (today also Vipps and Trustly), `Invoice` (invoice and KID/OCR), `Instalment` (part payment and financing), `NotPaid` or `Unknown`. |
| `Metadata` | object (string → string) | `metadata` from the order, as you sent it. |

**Example**

```json
{
  "id": "7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14",
  "type": "Order",
  "merchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844",
  "status": "Completed",
  "data": {
    "CreatedDate": "2026-09-17T09:12:03Z",
    "Reference": "visit-48213",
    "CompletedDate": "2026-09-17T09:14:40Z",
    "PaymentMethod": "Swish",
    "Metadata": { "bookingId": "48213" }
  }
}
```

### The order is cancelled

- status: `Cancelled`
- type: `Order`, `Invoice`
- Paths: The order's callbackUrl · The clinic's webhook address · Your organisation's endpoints
- Guides: [Cancel and refund](https://developers.rivr.io/guides/cancel-refund.md)

**When it is sent.** When the order is cancelled: by the clinic or your system, by the patient, or when the time to pay has run out. A cancelled order can be reopened and then sends `Created`.

**The fields in data**

| Field | Type | Description |
| --- | --- | --- |
| `CreatedDate` | string (date-time) | When the order was created. |
| `Reference` | string \| null | Your reference from the order. |
| `CancelledDate` | string (date-time) \| null | When the order was cancelled. |
| `Reason` | string | `MerchantCancelled`, `UserCancelled` or `Timeout`. |
| `Metadata` | object (string → string) | `metadata` from the order, as you sent it. |

**Example**

```json
{
  "id": "7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14",
  "type": "Order",
  "merchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844",
  "status": "Cancelled",
  "data": {
    "CreatedDate": "2026-09-17T09:12:03Z",
    "Reference": "visit-48213",
    "CancelledDate": "2026-09-17T11:02:17Z",
    "Reason": "MerchantCancelled",
    "Metadata": { "bookingId": "48213" }
  }
}
```

### The order is refunded

- status: `Refunded`
- type: `Order`, `Invoice`
- Paths: The order's callbackUrl · The clinic's webhook address · Your organisation's endpoints
- Guides: [Cancel and refund](https://developers.rivr.io/guides/cancel-refund.md)

**When it is sent.** When the order has been refunded. For some payment methods only once the payment provider has confirmed the refund.

**The fields in data**

| Field | Type | Description |
| --- | --- | --- |
| `CreatedDate` | string (date-time) | When the order was created. |
| `Reference` | string \| null | Your reference from the order. |
| `RefundedDate` | string (date-time) \| null | When the order was refunded. |
| `Metadata` | object (string → string) | `metadata` from the order, as you sent it. |

**Example**

```json
{
  "id": "7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14",
  "type": "Order",
  "merchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844",
  "status": "Refunded",
  "data": {
    "CreatedDate": "2026-09-17T09:12:03Z",
    "Reference": "visit-48213",
    "RefundedDate": "2026-09-18T08:30:51Z",
    "Metadata": { "bookingId": "48213" }
  }
}
```

### An onboarding has moved on

- status: The onboarding's status, as `GET /api/public/onboarding-links/{id}` gives it, for example `signed`, `in_review` or `active`.
- type: `MerchantOnboarding`
- Paths: The platform's onboarding address
- Guides: [Connect a clinic from your system](https://developers.rivr.io/guides/onboarding-links.md)

**When it is sent.** When an onboarding the platform started has changed after the agreement was signed: its status, `ChargesEnabled` or `PayoutsEnabled`. The same state is not sent twice, and the notification can come hours after the change, since Rivr goes through the states again after the nightly validation. It is an add-on: `GET` on the onboarding always answers right.

**The fields in data**

| Field | Type | Description |
| --- | --- | --- |
| `OnboardingLinkId` | string (uuid) | The onboarding's id, the same as `id`. |
| `OrganisationNumber` | string | The clinic's organisation number. |
| `Status` | string | The same as `status`. |
| `PreviousStatus` | string \| null | The status in the previous notification, or null. |
| `Requirements` | array of string | What is missing before the clinic can take payments, in the clinic's language. Empty when nothing is waiting. |
| `ChargesEnabled` | boolean | The clinic can take payments: the signal to start sending orders. |
| `PayoutsEnabled` | boolean | Payouts to the clinic's account are allowed. |
| `MerchantId` | string (uuid) | The clinic, the same as `merchantId`. |

**Example**

```json
{
  "id": "0b8e6f3c-5d2a-4e71-9c4b-7a1f2d3e4c5b",
  "type": "MerchantOnboarding",
  "merchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844",
  "status": "active",
  "data": {
    "OnboardingLinkId": "0b8e6f3c-5d2a-4e71-9c4b-7a1f2d3e4c5b",
    "OrganisationNumber": "5566778899",
    "Status": "active",
    "PreviousStatus": "in_review",
    "Requirements": [],
    "ChargesEnabled": true,
    "PayoutsEnabled": true,
    "MerchantId": "aa2225b7-de35-4afa-bd9b-8a47577ce844"
  }
}
```

The ids and times in the examples are made up.

## What differs from the API's answers

### status is a string in the webhook, a number in GET

In the webhook `status` is a string: `Completed`. In `GET /api/public/orders/{orderId}` `status` is a number: 1 `Created`, 2 `Pending`, 4 `Completed`, 8 `Refunded` and 32 `Cancelled`.

### The payment method is grouped differently

In the webhook `PaymentMethod` is a name, and Vipps and Trustly show as `Swish`. In `GET /api/public/orders/{orderId}` `paymentMethod` is a number (0 `NotPaid`, 1 `Card`, 2 `Swish`, 3 `Invoice`, 4 `Instalment`, 8 `Unknown`), and there Vipps and Trustly show as 1 `Card`. Don't build logic that needs the two to agree.

### PascalCase in data, camelCase everywhere else

The envelope (`id`, `type`, `merchantId`, `status`, `data`) and the API's answers are camelCase. The keys in `data` are PascalCase: `CompletedDate`, not `completedDate`.

### The same status can come again

Delivery is at least once and without an ordering guarantee, and `Rivr-Webhook-Id` is the same on every retry to the same receiver but differs between paths. A webhook can also carry the order's current status without anything having changed, for example when a refund is stopped because the clinic has locked the order. Move the journal only when the order's status has actually changed.

## Delivery errors

The delivery log for your organisation's endpoints, under Webhooks in the portal, shows an error line per delivery and per attempt. Here is every line, whether Rivr tries again, the cause and what to do. In live the line is cut down to a fixed form: the host in the address error and everything after the status code are not shown, and text that is not one of the lines is shown as `delivery failed`.

### The endpoint was deleted

- Line: `organization endpoint was deleted`
- Rivr tries again: No
- On the side of: you

**Cause.** The endpoint was deleted after the delivery was queued. Rivr does not try again.

**What to do.** Create a new endpoint if you want to receive the events.

### The endpoint is paused

- Line: `organization endpoint missing or inactive`
- Rivr tries again: Yes
- On the side of: you

**Cause.** The endpoint is missing or inactive when Rivr tries to deliver, for example because it is paused. Rivr tries again until the delivery gives up after 14 attempts, about seven days.

**What to do.** Resume the endpoint before Rivr gives up, after about seven days. The deliveries that are waiting are sent then.

### The endpoint no longer takes the event

- Line: `organization endpoint no longer subscribes to this clinic or event`
- Rivr tries again: No
- On the side of: you

**Cause.** The endpoint's event or clinic filter was changed after the delivery was queued, so that it no longer lets the clinic or the event through. Rivr does not try again.

**What to do.** Change the filter if you want the event, and replay the delivery in the sandbox. In live there is no replay: fetch the order with `GET /api/public/orders/{orderId}`.

### No signing key could be used

- Line: `no usable signing key for endpoint`
- Rivr tries again: No
- On the side of: Rivr

**Cause.** Rivr could not use the endpoint's signing key, so the delivery was not sent. The fault is on Rivr's side. Rivr does not try again.

**What to do.** Rotate the endpoint's signing key. If the error stays, contact Rivr.

### The address is blocked or not https

- Line: `blocked or non-HTTPS target URL`
- Rivr tries again: No
- On the side of: you

**Cause.** The endpoint's address is blocked or does not use https. Rivr does not try again.

**What to do.** The address must be https and reach a public address.

### The host only resolves to non-public addresses

- Line: `The webhook host {host} resolves only to addresses that are not public.`
- Rivr tries again: No
- On the side of: you

**Cause.** The endpoint's host only resolves to addresses that are not public, and Rivr does not send there. Rivr does not try again.

**What to do.** Use a host that is reachable at a public address.

### The receiver rejected the delivery

- Line: `integrator responded {code} (permanent rejection, not retried)`
- Rivr tries again: No
- On the side of: you

**Cause.** Your server answered 400, 405, 410, 413, 414, 415 or 422: the receiver rejected the body. Rivr does not try again.

**What to do.** Fix the receiver, then replay the delivery in the sandbox. In live there is no replay: fetch the order with `GET /api/public/orders/{orderId}`. A wrong signature should get 401, not 400, so that Rivr tries again.

### The receiver did not answer 2xx

- Line: `integrator responded {code}`
- Rivr tries again: Yes
- On the side of: you

**Cause.** Your server answered with a status code other than 2xx, for example 401, 403, 404, 408, 409, 425, 429 or 5xx. A redirect (3xx) is not followed and also counts as a failure. Rivr tries again.

**What to do.** Fix what makes the server answer like that, and answer 2xx once the event is received. If the delivery has already given up you can replay it in the sandbox, and fetch the order with `GET /api/public/orders/{orderId}` in live.

### The receiver did not answer within 5 seconds

- Line: `Mottagaren svarade inte inom 5 sekunder och anropet avbröts (timeout).`
- Rivr tries again: Yes
- On the side of: you

**Cause.** Your server did not answer within 5 seconds, so Rivr cancelled the call. The same line is also written when the connection was refused or a network error cut the call, and it is in Swedish even in the English environment. Rivr tries again.

**What to do.** Answer 2xx within 5 seconds and do the work afterwards. Also check that the server can be reached.

### Expired without being sent

- Line: `expired: never sent within 7 days (organization webhook delivery was switched off)`
- Rivr tries again: No
- On the side of: Rivr

**Cause.** The delivery was never sent within seven days, because Rivr's delivery to organisations' endpoints was switched off. The fault is on Rivr's side. Rivr does not try again.

**What to do.** Fetch the order with `GET /api/public/orders/{orderId}`.

### Deferred, delivery is switched off

- Line: `deferred: organization webhook delivery is switched off (OrgWebhookDeliveryEnabled)`
- Rivr tries again: Yes
- On the side of: Rivr

**Cause.** Rivr's delivery to organisations' endpoints is switched off, so the delivery waits. The fault is on Rivr's side. Rivr tries again in one minute.

**What to do.** Nothing for you to do.
