# Conventions

These rules hold for every endpoint in the API.

## Requests

Request and response bodies are JSON, so send `Content-Type: application/json`. The one exception is document uploads,
where the request body is the file itself. See [Documents](/companies/#documents).

Every request sends a token in the `Authorization` header. `POST /v1/login`, `GET /v1/catalog/files`, and
`GET /v1/catalog/search` send the app token. Every other endpoint sends the user token. See
[Authentication](/authentication/).

## IDs

Every Beauty Router ID is a prefix, a hyphen, and a base62 string, for example `prod-4Fz9QbX2kL`. Treat IDs as opaque
strings. Do not parse them or assume their length.

| Prefix  | Object                                              |
| ------- | --------------------------------------------------- |
| `loc`   | Salon location, which is a company in Beauty Router |
| `user`  | User                                                |
| `lic`   | License                                             |
| `exc`   | Exemption certificate                               |
| `order` | Order                                               |
| `prod`  | Product                                             |
| `group` | Product group                                       |
| `brand` | Brand                                               |
| `sup`   | Supplier, which is a brand or distributor           |
| `cat`   | Category                                            |
| `match` | Product match                                       |
| `svc`   | Service                                             |
| `event` | Webhook event                                       |
| `card`  | Payment card                                        |

### Your own IDs

You send your own IDs as `externalSalonLocationId` and `externalUserId` when you [log in a user](/authentication/), and as
`externalId` when you [send products for matching](/product-matching/). Each one is any string up to 128 characters.

## Timestamps

Timestamps are ISO 8601 in UTC, for example `2026-10-28T19:03:55Z`. Dates without a time, such as a license expiration
date, are ISO 8601 dates, for example `2027-06-30`.

Always send timestamps in UTC, ending in `Z`. A timestamp with a time zone, such as `2026-10-28T12:03:55-07:00`, fails
with `validation_failed`, because it can lead to errors.

## Money

Amounts are decimal numbers in dollars, with a `currency` field next to them. The currency is always `"usd"`.

```json
{
  "total": 275.01,
  "currency": "usd"
}
```

Each currency has a fixed precision. For `usd` it is two decimal places. An amount with more decimal places than the
currency supports fails with `validation_failed`.

## Partial updates

The API does not use `PATCH`. An endpoint accepts a partial update only when its documentation says so, as
`PUT /v1/company` does. Send `PUT` with only the fields you change, and fields you leave out stay as they are.

```bash
curl -X PUT https://api.beautyrouter.com/v1/company \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "displayName": "Golden Hour Hair Studio" }'
```

Every other `PUT` endpoint takes the full value.

## Paging

Only some endpoints support paging. Each endpoint's documentation says whether it does. Those endpoints use cursor paging
with two query parameters:

| Parameter       | Description                                                             |
| --------------- | ----------------------------------------------------------------------- |
| `limit`         | How many items to return. The default is 50, and the maximum is 200.    |
| `startingAfter` | The ID of the last item on the previous page. Leave it out for page 1. |

A paged list returns `data` with the items and `hasMore`, which is `true` when another page follows.

```bash
curl "https://api.beautyrouter.com/v1/product-matches?limit=100" \
  -H "Authorization: Bearer $USER_TOKEN"
```

```json
{
  "data": [
    {
      "id": "match-3Pa8sDf2Gh",
      "externalId": "inv-10482",
      "status": "matched",
      "productId": "prod-4Fz9QbX2kL",
      "createdAt": "2026-10-27T09:14:02Z",
      "updatedAt": "2026-10-27T09:14:02Z"
    },
    {
      "id": "match-9Lk4jHg7Fd",
      "externalId": "inv-10517",
      "status": "needs_reply",
      "question": "Is this product from Jadewell or Amberlin?",
      "createdAt": "2026-10-27T09:14:02Z",
      "updatedAt": "2026-10-27T16:40:31Z"
    }
  ],
  "hasMore": true
}
```

To get the next page, send the ID of the last item as `startingAfter`:

```bash
curl "https://api.beautyrouter.com/v1/product-matches?limit=100&startingAfter=match-9Lk4jHg7Fd" \
  -H "Authorization: Bearer $USER_TOKEN"
```

## Errors

An error has an HTTP status and a body with an `error` object:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "ein must have 9 digits.",
    "field": "ein"
  }
}
```

| Field     | Description                                                                        |
| --------- | ---------------------------------------------------------------------------------- |
| `code`    | A stable [code](/errors/). Use it in your code.                                    |
| `message` | A sentence for developers. It can change, so do not match on it.                   |
| `field`   | The request field that caused the error. Only some errors have it.                 |
| `missing` | Only on `verification_data_missing` and `app_setup_incomplete`: what still needs to be set. |
| `quote`   | Only on `order_total_changed` and `order_has_problems`: a fresh [quote](/create-orders/#get-a-quote). |

[Errors](/errors/) lists every code, with what it means and how to fix it.

For a `401` with `user_token_expired` or `invalid_user_token`, log the user in again with `POST /v1/login` and repeat
the request. See [Authentication](/authentication/#token-expiry).