# POST /api/public/onboarding-links

> Create onboarding link

Starts an onboarding and returns the link the clinic should open.

Only the organisation number is required. Send what you know about the clinic to shorten its way through the
flow, but who may sign is always decided by the company's signatory rights in the company registry and a BankID
signature, never by what you send in.

If the clinic already has an onboarding with you that can still be opened, you get that onboarding back instead of
a new one (the status is `200` either way), so a button that is clicked twice does not give the clinic two
agreements to choose between.

- Surface: `/api/public` · operationId `createOnboardingLink` · Group: OnboardingLinks
- Authorisation: Token with scope `public:onboarding:write`
- Organisation key: can't reach this route: the key check answers 403 `route_not_available`. Use a platform token for it.
- The spec: https://developers.rivr.io/openapi/api-public.json
- The spec's introduction: https://developers.rivr.io/reference/api-public.md
- Page in the portal: https://developers.rivr.io/reference/api-public/createOnboardingLink

The examples target the sandbox (`https://api.test.rivr.io`) and need test credentials.

## Request body (`application/json`, optional)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `organisationNumber` | string | – | The clinic's organisation number. Swedish or Norwegian, with or without a hyphen. |
| `companyName` | string | – | The company name you use for the clinic. Only for your own recognition in the list: the agreement is always written in the registered name that Rivr fetches from the company registry. (nullable) |
| `email` | string | – | The clinic's email address. Required only if you want Rivr to email the link (`sendEmail`); otherwise you get the link in the response and show it yourself. (nullable) |
| `phone` | string | – | The clinic's phone number. (nullable) |
| `contactName` | string | – | The name of your contact at the clinic. Prefills the email. Who may actually sign is decided by the company's signatory rights and BankID, never by this field. (nullable) |
| `priceListId` | string (uuid) | – | Which of your allowed price lists the clinic should get. Left out means your standard price list for the clinic's market, or your default. A price list outside your allowed ones, or in another currency than the clinic's market (SEK in Sweden, NOK in Norway), is rejected: Rivr sets the price lists, not you. (nullable) |
| `sendEmail` | boolean | – | Let Rivr email the link to `email`, with a reminder every fifth day (at most two). Defaults to `false`: the usual model is that you show the link in your own interface to a signed-in clinic user, which converts better and keeps the link out of email. |

**Example**

```json
{
  "organisationNumber": "string",
  "companyName": "string",
  "email": "kontakt@example.com",
  "phone": "+46701740605",
  "contactName": "string",
  "priceListId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "sendEmail": false
}
```

## Responses

### 200

OK

`application/json`

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `id` | string (uuid) | Your stable handle on the onboarding. Keep it: it is the key for the status lookups, and it stays the same when the clinic changes email address or gets a new link. |
| `organisationNumber` | string | The clinic's organisation number as Rivr normalises it: a Swedish number as `NNNNNN-NNNN`, with a hyphen, and a Norwegian number as nine digits. |
| `companyName` | string | The company name stored on the onboarding, for recognising it in a list. `null` when none is known. (nullable) |
| `url` | string | The link the clinic should open. Set as long as it can be opened, and `null` when the onboarding is signed, expired or revoked, because there is nothing to open then. Show it to a signed-in user in your own interface; do not forward it by email. (nullable) |
| `expiresAt` | string (date-time) | When the link stops working. After that, `POST /onboarding-links/{id}/refresh` gives a new link with the same id and a new expiry time. (nullable) |
| `status` | string | Where the clinic is in the onboarding. One of `pending` (the link is issued but the clinic has not opened it), `opened` (the clinic has opened the link but not finished signing), `signed` (the agreement is signed with BankID and the merchant exists; a short step while the payment setup is prepared), `in_review` (the payment provider is reviewing, or waiting for something in `requirements`), `active` (the setup is complete; send orders once `chargesEnabled` is true), `expired` (the link expired before the clinic signed; `refresh` gives a new one) or `revoked` (cancelled by Rivr or by you, and final: create a new onboarding instead). |
| `requirements` | string[] | What remains before the clinic can take payments, as short texts in the clinic's language (Swedish or Norwegian) for showing to a person. They are not stable values: do not program against them. Empty when nothing is pending. Meaningful when `status` is `in_review`, and when it is `active` and `chargesEnabled` is false, where it only says that Rivr is reviewing the details. |
| `chargesEnabled` | boolean | The clinic can take payments. This is the signal to start sending orders for `merchantId`, not `status`. Rivr's counterpart to Stripe's `charges_enabled`. False before signing. After signing it follows the same rule that Rivr's payment flows use to let a payment through, so an order created while the flag is true does not bounce because the clinic is not ready. When Rivr cannot decide at the moment, the flag is false, never true on a guess. |
| `payoutsEnabled` | boolean | The payment provider allows payouts to the clinic's bank account: the payout capability is both allowed and verified. Rivr's counterpart to Stripe's `payouts_enabled`. For information only: it does not decide which orders you can send. |
| `merchantId` | string (uuid) | The clinic's merchant id, created when the agreement was signed. Use it as `merchant_id` when you request a merchant token, and as `merchantId` when you create orders for the clinic. `null` until the agreement is signed. (nullable) |
| `createdAt` | string (date-time) | When the onboarding was created. |
| `signedAt` | string (date-time) | When the agreement was signed with BankID and the merchant was created. (nullable) |
| `emailSent` | boolean | True when Rivr emailed the link to the clinic on your behalf (`sendEmail`). False when you got the link to show yourself. |

**Example**

```json
{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "organisationNumber": "string",
  "companyName": "string",
  "url": "string",
  "expiresAt": "2026-10-18T09:00:00Z",
  "status": "string",
  "requirements": [
    "string"
  ],
  "chargesEnabled": false,
  "payoutsEnabled": false,
  "merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "createdAt": "2026-10-18T09:00:00Z",
  "signedAt": "2026-10-18T09:00:00Z",
  "emailSent": false
}
```

### 400

The onboarding was not started: the organisation number is not valid (`organisation_number_invalid`), a field is too
long (`field_too_long`, with the field in `propertyName`), `sendEmail` is set without `email`
(`send_email_requires_email`), the price list is not one your platform may use (`price_list_not_allowed`), the price
list is in another currency than the clinic's market (`price_list_wrong_market`), or the
link could not be issued (`onboarding_link_not_issued`). The body is the error object. A request that cannot be read is rejected before the operation runs, with a validation problem instead: malformed JSON, a value of the wrong type, an empty body or a `null` required field. It carries the `code` `request_invalid`, `type`, `message`, `title`, `status`, `errors` (the messages per field) and `traceId`.

Error codes: [request_invalid](https://developers.rivr.io/errors/request_invalid.md), [organisation_number_invalid](https://developers.rivr.io/errors/organisation_number_invalid.md), [field_too_long](https://developers.rivr.io/errors/field_too_long.md), [send_email_requires_email](https://developers.rivr.io/errors/send_email_requires_email.md), [price_list_not_allowed](https://developers.rivr.io/errors/price_list_not_allowed.md), [price_list_wrong_market](https://developers.rivr.io/errors/price_list_wrong_market.md), [onboarding_link_not_issued](https://developers.rivr.io/errors/onboarding_link_not_issued.md)

`application/json`

```json
{
  "propertyName": "organisationNumber",
  "message": "The organisation number is not valid.",
  "errorCode": "organisation_number_invalid",
  "code": "organisation_number_invalid",
  "type": "https://developers.rivr.io/errors/organisation_number_invalid"
}
```

### 401

The token is missing (`token_missing`), not valid (`token_invalid`) or expired (`token_expired`), or it is not a
platform token, for example a merchant token (`platform_token_required`).

Error codes: [token_missing](https://developers.rivr.io/errors/token_missing.md), [token_invalid](https://developers.rivr.io/errors/token_invalid.md), [token_expired](https://developers.rivr.io/errors/token_expired.md), [platform_token_required](https://developers.rivr.io/errors/platform_token_required.md)

`application/json`

```json
{
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid Authorization Header",
  "propertyName": null,
  "message": "The request has no valid Authorization header. Send the token in the Authorization header, after Bearer.",
  "errorCode": "token_missing",
  "code": "token_missing",
  "type": "https://developers.rivr.io/errors/token_missing"
}
```

### 403

The token lacks the scope (`invalid_scope`, with `error` and `error_description` as before), or onboarding is not
enabled for your platform (`onboarding_not_enabled`).

Error codes: [invalid_scope](https://developers.rivr.io/errors/invalid_scope.md), [onboarding_not_enabled](https://developers.rivr.io/errors/onboarding_not_enabled.md)

`application/json`

```json
{
  "propertyName": null,
  "message": "Onboarding through the API is not enabled for your platform. Contact Rivr.",
  "errorCode": "onboarding_not_enabled",
  "code": "onboarding_not_enabled",
  "type": "https://developers.rivr.io/errors/onboarding_not_enabled"
}
```

### 415

The body is not sent as `application/json`: `unsupported_media_type`.

Error codes: [unsupported_media_type](https://developers.rivr.io/errors/unsupported_media_type.md)

`application/json`

```json
{
  "type": "https://developers.rivr.io/errors/unsupported_media_type",
  "title": "Unsupported Media Type",
  "status": 415,
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
  "code": "unsupported_media_type",
  "message": "The body's Content-Type is not one the operation reads. Send the body as application/json."
}
```

### 500

Something unexpected failed: `internal_error`.

Error codes: [internal_error](https://developers.rivr.io/errors/internal_error.md)

`application/json`

```json
{
  "propertyName": null,
  "message": "Something unexpected failed at Rivr, and whether the request was carried out is not known. Check the order or what the request was about before you retry.",
  "errorCode": "internal_error",
  "code": "internal_error",
  "type": "https://developers.rivr.io/errors/internal_error"
}
```

## Example request

```bash
curl -X POST https://api.test.rivr.io/api/public/onboarding-links \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "organisationNumber": "string",
  "companyName": "string",
  "email": "kontakt@example.com",
  "phone": "+46701740605",
  "contactName": "string",
  "priceListId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "sendEmail": false
}'
```
