# Error codes

The errors that the key check in front of `/api/public` decides itself are `application/problem+json` with a stable `code`. `/api/public` answers with its own error object, which also carries `code` and `type`: the order endpoints since 2026-10-21 and every error answer since 2026-10-25. Program against `code`, not against the texts. A few errors come before that and carry no `code`, see the bottom of the page. The account connection follows OAuth 2.0 instead and has a group of its own.

As JSON: https://developers.rivr.io/errors.json

## What an error looks like

- `type`: The link to the code's page here.
- `title`: A short heading for humans. May change.
- `status`: The HTTP status, same as the response's.
- `detail`: What happened in this very call, for humans. May change.
- `instance`: The path that was called.
- `code`: The stable code. Program against it.
- `requestId`: The call's id at Rivr, the same as the `Rivr-Request-Id` response header. Quote it when you contact Rivr.
- `retryWhen`: In the key check in front of `/api/public` since 2026-10-29: whether and when the very same call can be sent again, `same_request`, `later`, `after_fix` or `never`, the same value as the code's page and /errors.json give. The `Rivr-Should-Retry` header is `true` only for `same_request`.

Errors follow RFC 9457. Some codes carry more members. They are listed on the code's page.

## The error object in /api/public

- `propertyName`: The field the error is about, or `null` when it is about none.
- `message`: What happened, for humans. May change.
- `errorCode`: The same code as `code`. Kept for existing integrations.
- `code`: The stable code. Program against it.
- `type`: The link to the code's page here.

Since 2026-10-25 every error answer under `/api/public` carries `code`, `type` and `message`, and the status is the same as before. Answers about the token and the scope keep the members they had before (`error` and `error_description`, or `title`, `status` and `detail`) ahead of the error object's. A body that can't be read, such as invalid JSON, gets the framework's validation problem with `errors` and `code` `request_invalid`.

## Retrying, machine-readable

Every code has the field `retryWhen` in `/errors.json` and on the code's page as Markdown. It says whether and when another attempt helps, with one of four values:

- `same_request`: Send the very same request again, unchanged: the same `Idempotency-Key` and the same order id. Wait `Retry-After` seconds when the answer has the header, otherwise a short while that grows with each attempt.
- `later`: The same request can succeed, but only once something outside it has changed: another payment has finished, a permission has been given or Rivr has switched something on. Sending it again straight away gets the same answer. Follow the order or wait for the change.
- `after_fix`: Change the request first, as the code's page says: a value, the token, a header or a new key. Then send it.
- `never`: Don't send it again. The answer is final for this request: read the current state, the order for example, and act on that.

## Did anything happen, machine-readable

Every code has the field `outcome` in `/errors.json` and on the code's page as Markdown. It says what the answer means for what you asked for, with one of five values. `outcomeNote` says what else may have happened, and `resolveWith` which read shows the state, per operation:

- `none` (It did not happen): This call did not do what you asked for and changed nothing else that you can see or that concerns money. Where the code has an `outcomeNote`, it takes precedence.
- `partial` (Something else happened): What you asked for did not happen, but something else had already happened, see the note.
- `unknown` (Unknown): The answer doesn't say whether what you asked for has happened, through this call or an earlier one. Read the state first.
- `done` (Already done): What you asked for has already happened, or the state already is what you asked for.
- `not_applicable` (No side effect): The call has no side effect, since it is a read.

## Access and organisation

- [token_missing](https://developers.rivr.io/errors/token_missing.md): 401 · The token is missing · `outcome`: `none`
- [token_invalid](https://developers.rivr.io/errors/token_invalid.md): 401 · The token is not valid · `outcome`: `none`
- [token_expired](https://developers.rivr.io/errors/token_expired.md): 401 · The token has expired · `outcome`: `none`
- [key_invalid](https://developers.rivr.io/errors/key_invalid.md): 401 · The key's token is not valid · `outcome`: `none`
- [organization_required](https://developers.rivr.io/errors/organization_required.md): 400 · Organisation missing · `outcome`: `none`
- [organization_invalid](https://developers.rivr.io/errors/organization_invalid.md): 400 · Invalid organisation · `outcome`: `none`
- [organization_not_accessible](https://developers.rivr.io/errors/organization_not_accessible.md): 403 · Organisation not accessible · `outcome`: `none`
- [route_not_available](https://developers.rivr.io/errors/route_not_available.md): 403 · Route closed to keys · `outcome`: `none`
- [merchant_token_required](https://developers.rivr.io/errors/merchant_token_required.md): 401 · The token isn't issued for a clinic · `outcome`: `none`
- [platform_token_required](https://developers.rivr.io/errors/platform_token_required.md): 401 · The token isn't a platform token · `outcome`: `none`
- [access_denied](https://developers.rivr.io/errors/access_denied.md): 302/401 · Access was denied · `outcome`: `none`
- [invalid_scope](https://developers.rivr.io/errors/invalid_scope.md): 403 · The token lacks the scope · `outcome`: `none`
- [clinic_token_required](https://developers.rivr.io/errors/clinic_token_required.md): 403 · Only clinic tokens are accepted · `outcome`: `none`
- [merchant_mismatch](https://developers.rivr.io/errors/merchant_mismatch.md): 403 · The resource belongs to another clinic · `outcome`: `none`
- [forbidden](https://developers.rivr.io/errors/forbidden.md): 403 · The call isn't allowed · `outcome`: `none`

## The request

- [rate_limited](https://developers.rivr.io/errors/rate_limited.md): 429 · Too many calls · `outcome`: `none`
- [not_found](https://developers.rivr.io/errors/not_found.md): 404 · Not found · `outcome`: `none`
- [request_invalid](https://developers.rivr.io/errors/request_invalid.md): 400 · The request can't be read · `outcome`: `none`
- [unsupported_media_type](https://developers.rivr.io/errors/unsupported_media_type.md): 415 · Wrong media type · `outcome`: `none`
- [route_not_found](https://developers.rivr.io/errors/route_not_found.md): 404 · No such route · `outcome`: `none`
- [request_rejected](https://developers.rivr.io/errors/request_rejected.md): 400 · Rivr refused the request · `outcome`: `unknown`
- [order_id_invalid](https://developers.rivr.io/errors/order_id_invalid.md): 400 · The order id isn't a UUID · `outcome`: `none`
- [receipt_id_invalid](https://developers.rivr.io/errors/receipt_id_invalid.md): 400 · The receipt id isn't a UUID · `outcome`: `not_applicable`
- [merchant_not_found](https://developers.rivr.io/errors/merchant_not_found.md): 404 · Clinic not found · `outcome`: `not_applicable`
- [subscription_not_found](https://developers.rivr.io/errors/subscription_not_found.md): 404 · Subscription not found · `outcome`: `not_applicable`

## Payments in Rivr Go

- [currency_not_supported](https://developers.rivr.io/errors/currency_not_supported.md): 422 · Currency not supported · `outcome`: `none`
- [payment_in_progress](https://developers.rivr.io/errors/payment_in_progress.md): 409 · A payment is already in progress · `outcome`: `unknown`
- [phone_required](https://developers.rivr.io/errors/phone_required.md): 422 · Phone number missing · `outcome`: `none`

## Creating an order in /api/public

- [personal_number_not_identifiable](https://developers.rivr.io/errors/personal_number_not_identifiable.md): 422 · The personal number can't be identified · `outcome`: `none`
- [personal_number_required](https://developers.rivr.io/errors/personal_number_required.md): 422 · Personal number missing · `outcome`: `none`
- [personal_number_not_test_identity](https://developers.rivr.io/errors/personal_number_not_test_identity.md): 422 · The personal number isn't a test identity · `outcome`: `none`
- [currency_code_invalid](https://developers.rivr.io/errors/currency_code_invalid.md): 422 · Invalid currency code · `outcome`: `none`
- [amount_zero_or_negative](https://developers.rivr.io/errors/amount_zero_or_negative.md): 422 · The amount is zero or negative · `outcome`: `none`
- [vat_percentage_negative](https://developers.rivr.io/errors/vat_percentage_negative.md): 422 · Negative VAT rate · `outcome`: `none`
- [discount_negative](https://developers.rivr.io/errors/discount_negative.md): 422 · Negative discount · `outcome`: `none`
- [discount_exceeds_line_total](https://developers.rivr.io/errors/discount_exceeds_line_total.md): 422 · The discount exceeds the line · `outcome`: `none`
- [email_required](https://developers.rivr.io/errors/email_required.md): 422 · Email missing · `outcome`: `none`
- [reference_required](https://developers.rivr.io/errors/reference_required.md): 422 · Reference missing · `outcome`: `none`
- [checkout_hint_redirect_invalid](https://developers.rivr.io/errors/checkout_hint_redirect_invalid.md): 422 · Invalid redirect address · `outcome`: `none`
- [checkout_hint_cancel_redirect_invalid](https://developers.rivr.io/errors/checkout_hint_cancel_redirect_invalid.md): 422 · Invalid cancel address · `outcome`: `none`
- [callback_url_invalid](https://developers.rivr.io/errors/callback_url_invalid.md): 422 · Invalid callback address · `outcome`: `none`
- [merchant_id_mismatch](https://developers.rivr.io/errors/merchant_id_mismatch.md): 400 · The clinic in the body isn't the token's · `outcome`: `none`
- [order_modified_concurrently](https://developers.rivr.io/errors/order_modified_concurrently.md): 409 · The order was written at the same time · `outcome`: `partial`
- [credit_check_failed](https://developers.rivr.io/errors/credit_check_failed.md): 400 · The credit check wasn't approved · `outcome`: `partial`

## Cancelling and refunding in /api/public

- [order_not_found](https://developers.rivr.io/errors/order_not_found.md): 404 · Order not found · `outcome`: `none`
- [order_paid](https://developers.rivr.io/errors/order_paid.md): 400 · The order is paid · `outcome`: `none`
- [payment_pending](https://developers.rivr.io/errors/payment_pending.md): 400 · A payment is under way · `outcome`: `unknown`
- [order_refunded](https://developers.rivr.io/errors/order_refunded.md): 400 · The order is refunded · `outcome`: `done`
- [order_not_cancellable](https://developers.rivr.io/errors/order_not_cancellable.md): 400 · The order can't be cancelled · `outcome`: `none`
- [order_changed](https://developers.rivr.io/errors/order_changed.md): 409 · The order changed meanwhile · `outcome`: `none`
- [claim_transferred](https://developers.rivr.io/errors/claim_transferred.md): 400 · The claim has been sold · `outcome`: `none`
- [claim_check_unavailable](https://developers.rivr.io/errors/claim_check_unavailable.md): 409 · The claim could not be checked · `outcome`: `none`
- [order_locked_for_refund](https://developers.rivr.io/errors/order_locked_for_refund.md): 400 · The order is locked for refunds · `outcome`: `unknown`
- [order_not_refundable](https://developers.rivr.io/errors/order_not_refundable.md): 400 · The order can't be refunded · `outcome`: `unknown`

## Sending an order to a device in /api/public

- [device_id_required](https://developers.rivr.io/errors/device_id_required.md): 400 · deviceId missing · `outcome`: `none`
- [order_not_sendable](https://developers.rivr.io/errors/order_not_sendable.md): 400 · The order can't be sent to a device · `outcome`: `none`
- [device_not_found](https://developers.rivr.io/errors/device_not_found.md): 400 · No such device at the clinic · `outcome`: `none`
- [device_offline](https://developers.rivr.io/errors/device_offline.md): 409 · The device is offline · `outcome`: `none`

## Idempotency-Key in /api/public

- [idempotency_key_invalid](https://developers.rivr.io/errors/idempotency_key_invalid.md): 400 · Invalid Idempotency-Key · `outcome`: `none`
- [idempotency_mismatch](https://developers.rivr.io/errors/idempotency_mismatch.md): 422 · The key is already used · `outcome`: `none`
- [idempotency_in_progress](https://developers.rivr.io/errors/idempotency_in_progress.md): 409 · The request with the key is in progress · `outcome`: `unknown`
- [idempotency_unavailable](https://developers.rivr.io/errors/idempotency_unavailable.md): 503 · The key could not be checked · `outcome`: `none`
- [idempotency_outcome_unknown](https://developers.rivr.io/errors/idempotency_outcome_unknown.md): 500 · The outcome is unknown · `outcome`: `unknown`

## Onboarding links in /api/public

- [onboarding_not_enabled](https://developers.rivr.io/errors/onboarding_not_enabled.md): 403 · Onboarding isn't switched on · `outcome`: `none`
- [organisation_number_invalid](https://developers.rivr.io/errors/organisation_number_invalid.md): 400 · Invalid organisation number · `outcome`: `none`
- [field_too_long](https://developers.rivr.io/errors/field_too_long.md): 400 · A field is too long · `outcome`: `none`
- [send_email_requires_email](https://developers.rivr.io/errors/send_email_requires_email.md): 400 · Email missing for the email · `outcome`: `none`
- [price_list_not_allowed](https://developers.rivr.io/errors/price_list_not_allowed.md): 400 · The price list isn't your platform's · `outcome`: `none`
- [price_list_wrong_market](https://developers.rivr.io/errors/price_list_wrong_market.md): 400 · The price list is in the wrong currency · `outcome`: `none`
- [onboarding_link_not_issued](https://developers.rivr.io/errors/onboarding_link_not_issued.md): 400 · The link couldn't be issued · `outcome`: `partial`
- [onboarding_link_not_found](https://developers.rivr.io/errors/onboarding_link_not_found.md): 404 · Onboarding not found · `outcome`: `none`
- [onboarding_link_revoked](https://developers.rivr.io/errors/onboarding_link_revoked.md): 409 · The onboarding is revoked · `outcome`: `none`
- [onboarding_already_signed](https://developers.rivr.io/errors/onboarding_already_signed.md): 409 · The agreement is already signed · `outcome`: `none`

## The account connection

- [invalid_request](https://developers.rivr.io/errors/invalid_request.md): 400 · The request can't be read · `outcome`: `none`
- [invalid_client](https://developers.rivr.io/errors/invalid_client.md): 401 · The client wasn't accepted · `outcome`: `none`
- [invalid_grant](https://developers.rivr.io/errors/invalid_grant.md): 400 · The code couldn't be redeemed · `outcome`: `unknown`
- [unsupported_grant_type](https://developers.rivr.io/errors/unsupported_grant_type.md): 400 · The wrong grant_type · `outcome`: `none`
- [server_error](https://developers.rivr.io/errors/server_error.md): 500 · Something failed at Rivr · `outcome`: `unknown`
- [temporarily_unavailable](https://developers.rivr.io/errors/temporarily_unavailable.md): 302 · The connection can't be started at the moment · `outcome`: `none`
- [account_required](https://developers.rivr.io/errors/account_required.md): 302 · The clinic has no Rivr account · `outcome`: `none`
- [clinic_not_ready](https://developers.rivr.io/errors/clinic_not_ready.md): 302 · The clinic can't be connected yet · `outcome`: `none`
- [invalid_redirect_uri](https://developers.rivr.io/errors/invalid_redirect_uri.md): 400 · The address isn't registered · `outcome`: `none`
- [started_in_another_browser](https://developers.rivr.io/errors/started_in_another_browser.md): 400 · The attempt was started in another browser · `outcome`: `none`
- [cookies_required](https://developers.rivr.io/errors/cookies_required.md): 400 · The browser didn't accept the cookie · `outcome`: `none`
- [clinic_connect_no_consent](https://developers.rivr.io/errors/clinic_connect_no_consent.md): 409 · The connection wasn't made here · `outcome`: `none`
- [merchant_not_connected](https://developers.rivr.io/errors/merchant_not_connected.md): 404 · The clinic isn't connected to you · `outcome`: `none`
- [clinic_connect_request_not_found](https://developers.rivr.io/errors/clinic_connect_request_not_found.md): 404 · The request doesn't exist · `outcome`: `none`
- [clinic_connect_request_used](https://developers.rivr.io/errors/clinic_connect_request_used.md): 409 · The request is already used · `outcome`: `none`
- [clinic_connect_client_refused](https://developers.rivr.io/errors/clinic_connect_client_refused.md): 409 · The client may no longer connect · `outcome`: `none`
- [clinic_connect_not_a_sandbox_clinic](https://developers.rivr.io/errors/clinic_connect_not_a_sandbox_clinic.md): 403 · The clinic isn't a sandbox clinic · `outcome`: `none`
- [clinic_connect_other_platform](https://developers.rivr.io/errors/clinic_connect_other_platform.md): 409 · The clinic is on another platform · `outcome`: `none`
- [clinic_connect_forbidden](https://developers.rivr.io/errors/clinic_connect_forbidden.md): 403 · The clinic is suspended · `outcome`: `none`
- [environment_mismatch](https://developers.rivr.io/errors/environment_mismatch.md): 409 · The test environment points the wrong way · `outcome`: `none`

## Rivr's services

- [authentication_unavailable](https://developers.rivr.io/errors/authentication_unavailable.md): 401 · The token could not be checked · `outcome`: `none`
- [access_check_unavailable](https://developers.rivr.io/errors/access_check_unavailable.md): 503 · Permissions could not be checked · `outcome`: `none`
- [internal_error](https://developers.rivr.io/errors/internal_error.md): 500 · Unexpected error at Rivr · `outcome`: `unknown`
- [service_unavailable](https://developers.rivr.io/errors/service_unavailable.md): 503 · The data couldn't be read · `outcome`: `none`
- [service_not_ready](https://developers.rivr.io/errors/service_not_ready.md): 503 · The instance isn't ready · `outcome`: `not_applicable`

## Errors without a code

These errors happen before the API itself has decided anything. They carry no `code`. Let the HTTP status decide, and your system works against both production and the sandbox:

- `429` when the same token has sent too many calls on `/api/public`. The body is `application/problem+json` with `"error": "rate_limited"`. Wait as many seconds as the `Retry-After` header says and try again. The limits are under Rate limits below.

## Rate limits

Rivr counts calls per token. A token may make 1,200 calls a minute at an even pace, and at most 200 at once. Go over and the next call gets `429` with `Retry-After`: wait that many seconds and send the same call again. A `429` has not done anything, since it is refused before the call reaches the service behind.

- **The limit.** 1,200 calls a minute, that is 20 a second at an even pace. The bucket holds 200 calls, so a burst of up to 200 goes through at once.
- **Counted per.** Token. Not IP address and not client. A call without a token is not counted here.
- **Over the limit.** `429` with `Retry-After` in whole seconds, at least 1. Nothing is queued: the call is refused at once.
- **How much is left.** The answers don't say. There is no `RateLimit-Remaining` or any other `RateLimit-*` header, so the first sign is the `429`.
- **On or off.** A switch in Rivr's configuration decides whether the limit applies, for the whole of Rivr. While it is off there is no limit. Build for the limit anyway.
- **Where it is counted.** In public-api. Since 2026-10-26 every call on api.rivr.io under `/api/public` goes through public-api, so the limit applies to every call with a token. Behind `/api/public` there are also protections per IP address that are not published limits.

### What a 429 looks like

**/api/public**

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 1

{
  "type": "https://www.rfc-editor.org/rfc/rfc6585#section-4",
  "title": "Too Many Requests",
  "status": 429,
  "error": "rate_limited",
  "detail": "Too many requests with this token. Retry after 1 s.",
  "requestId": "5b2f0c9e-8d41-4a7e-9f36-2c1d7e0a4b18"
}
```

**What your system does.** Wait `Retry-After` seconds, then send the same call again. Spread bursts over time and take changes from webhooks instead of polling. Fetch a token once an hour and reuse it, rather than a new one per call.

The limits for every surface, the token endpoints, the portal and the MCP server too, are on the Rate limits page. https://developers.rivr.io/rate-limits.md
