# Cancel and refund

> Cancel an unpaid order, pay back a paid one — and wait for the webhook.

The patient cancels, the treatment didn't happen, or something was charged wrongly. What the journal does depends on where the order is: if it is unpaid you cancel it, if it is paid you refund it. Either way, the webhook is what confirms it.

- Market: Sweden (`se`). Norway differs in currency, payment methods and what happens when nobody pays: https://developers.rivr.io/guides/cancel-refund.md?market=no
- Language: English (`en`). Svenska: https://developers.rivr.io/guides/cancel-refund.md?lang=sv
- 5 min read · 5 steps
- Page in the portal: https://developers.rivr.io/guides/cancel-refund

Every example targets the sandbox (`https://api.test.rivr.io`) and needs test credentials. A platform client's tokens come from `https://auth.test.rivr.io`, an organisation key's from `https://auth.test.rivr.tech`.

## 1. Unpaid: cancel

*Journal system → Rivr*

`POST /api/public/orders/{id}/cancel` stops an unpaid order: no more texts, no reminder and no invoice. `204` means the order is cancelled — or already was.

**Why:** `/cancel` **never** moves money. If the order can't be cancelled, the response says why, so the journal can pick the right next step instead of guessing.

> **Note.** Send a new `Idempotency-Key` for every cancellation, and the same key when you retry the call. Without a key, a retry is safe only as long as the order stays cancelled. With a key, a late retry gets the stored `204` and does not cancel an order that has been opened again with `PUT` in the meantime. Both the sandbox and production accept the header.

**Request:** `POST /api/public/orders/7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14/cancel` · Merchant token · [Reference](https://developers.rivr.io/reference/api-public/cancelOrder.md)

```bash
curl -X POST https://api.test.rivr.io/api/public/orders/7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14/cancel \
  -H "Authorization: Bearer $MERCHANT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

**Response (excerpt)**

```
HTTP 400 { "errorCode": "order_paid" }             → already paid: use /refund
HTTP 400 { "errorCode": "payment_pending" }        → invoice, bank or financing payment under way
HTTP 400 { "errorCode": "order_refunded" }         → already refunded
HTTP 400 { "errorCode": "order_not_cancellable" }  → any other state that can't be cancelled
HTTP 409 { "errorCode": "payment_in_progress" }    → patient is paying on the device: retry shortly
HTTP 409 { "errorCode": "order_changed" }          → changed while cancelling: retry
HTTP 404 { "errorCode": "order_not_found" }
HTTP 403 { "error": "merchant_mismatch" }          → the order belongs to another clinic
```

## 2. The Cancelled webhook

*Rivr → Journal system*

`Cancelled` arrives with `Reason: "MerchantCancelled"` and `CancelledDate`. The same `Reason` is used today even when the clinic's flow cancels the order automatically, so it doesn't tell you who cancelled.

**What your system does:** Close the payment on the visit. Handle the receivable from the patient as you would for any cancellation.

**Order status:** `Cancelled` · **The journal shows:** Cancelled

**Webhook to you** (`Cancelled`)

```http
POST /webhooks/rivr
Content-Type: application/json
Rivr-Signature: t=1789640080,v1=5f2b0c…

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

## 3. Paid: refund

*Journal system → Rivr*

`POST /api/public/orders/{id}/refund` pays back the **whole** amount through the payment method the patient used. A `204` means the refund has started — not that it is done.

> **Warning.** Partial refunds are not available in the API. The answer is `400` for some payment methods — instalments through Medical Finance, for example — for orders the clinic has locked against refunds, and for `Pending` orders with financing or a bank payment under way. An order id that doesn't exist, or belongs to another clinic, gives `404` with `order_not_found`, the same as `/cancel`.

**Order status:** `Completed` · **The journal shows:** Refund in progress

**Request:** `POST /api/public/orders/7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14/refund` · Merchant token · [Reference](https://developers.rivr.io/reference/api-public/refundOrder.md)

```bash
curl -X POST https://api.test.rivr.io/api/public/orders/7b1e7c3a-2f4d-4a51-9c7e-5d0a6b8e2c14/refund \
  -H "Authorization: Bearer $MERCHANT_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

## 4. Money back to the patient

*Rivr → Patient*

Rivr pays back the way the patient paid: the card, Swish, Vipps or the invoice. For some payment methods the order becomes `Refunded` only once the payment provider has confirmed.

## 5. The Refunded webhook

*Rivr → Journal system*

`Refunded` confirms the money is on its way back. If the order had already been settled, the refund shows up in a later settlement under `refundedOrders`. If it was refunded before that night's settlement it shows up in no settlement at all — it was never part of a payout.

**What your system does:** Now, not before: mark the visit as refunded and reverse step A in the books — whether or not the refund later shows up in a settlement.

> **Rule.** Never update the status in the journal on your own call. Wait for the webhook.

**Order status:** `Refunded` · **The journal shows:** Refunded

**Webhook to you** (`Refunded`)

```http
POST /webhooks/rivr
Content-Type: application/json
Rivr-Signature: t=1789640080,v1=5f2b0c…

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

## Common pitfalls

- Using `/refund` on an unpaid order. It works — the order is cancelled — but `/cancel` tells you why when it can't.
- Trying to cancel a `Pending` order with `/cancel`. It answers `payment_pending` — use `/refund`, and wait for `Cancelled`, not `Refunded`, since the patient never paid. In that webhook `Reason` is empty and `CancelledDate` is `null`.
- Reusing an order id. A new `PUT` on a cancelled or refunded order reopens it as unpaid. Give every new payment a new id.

## Next guide

- [Payment in advance](https://developers.rivr.io/guides/prepayment.md): The patient pays before the treatment — the same flow, but the money becomes a credit.
- [Bookkeeping and payouts](https://developers.rivr.io/guides/bookkeeping.md): Two entries per order: when it is paid, and when the money reaches the bank.
- [Errors and retries](https://developers.rivr.io/guides/errors-retries.md): One way to handle errors against /api/public: read the code, decide whether to send the call again, wait the right time and send the same Idempotency-Key again.
