# Cliento API reference: Customers

Base URL: `https://api.cliento.com`

The Customers APIs let an integration keep a location's customer register in step with another
system: find customers, and create, change and delete them.

A customer is one of three types, each with its own API:

- **Persons.** A person is an individual: the one who books, and the one the location sends
  reminders to.
- **Companies.** A company owns a vehicle or pays the invoice.
- **Vehicles.** A vehicle is what a garage or a car wash works on.

The Customers API searches all three types at once. Use it when you have a phone number or a
reference and do not know which type it belongs to.

Each customer belongs to one location. The links between them, such as a vehicle's owner, are not
available through the API yet.

What staff record about a person over time, such as a recipe, belongs to that person.

## Persons

The Persons API lets an integration list a location's persons, find one, and create, change and
delete them.

A person is an individual customer: someone who books an appointment, or the contact person for a
company or a vehicle. The location communicates with persons, so the phone number, the email
address and the consents for SMS and email reminders are on the person.

A person belongs to one location and has an id of the form `per_...`. Reading needs the scope
`customers:read`, and writing needs `customers:write`.

### Finding a person

Filter the list. Each filter matches the whole value, and several together match persons that have
all of them:

- `?email=`, an email address. Letter case is ignored.
- `?phoneNumber=`, in E.164 format with the plus sign percent-encoded (`%2B46701234567`). A number
  that staff typed in another format is not found.
- `?identityNumber=`, a personnummer or other identity number as it is stored.
- `?externalReference=`, the id you gave the person when you created it.

Only `externalReference` is unique: no two persons, companies or vehicles at a location share one,
though the reference of a deleted one can be used again. Two persons can share an email address or
a phone number, and sometimes an identity number, so the other filters can return several persons
and you choose between them.

When you do not know whether you are looking for a person, a company or a vehicle, search
Customers instead.

### Identity numbers

A person can have one identity number issued by an authority. It is sent and returned as
`identityNumber`, with a `type` and a `number`. An `identityNumber` that is not `null` needs
both. `null` in an update removes it. The number is kept as you send it. Only surrounding spaces
and any HTML markup are removed, before it is checked and stored. A personnummer and a
samordningsnummer are digits only, with no hyphen or space.

| `type` | What it is | Accepted `number` | Example |
|---|---|---|---|
| `se_personnummer` | Swedish personal identity number | Twelve digits, `YYYYMMDDNNNC`: the full year, the day between 01 and 31, and a valid check digit | `198604122385` |
| `se_samordningsnummer` | Swedish coordination number | Twelve digits like a personnummer, with 60 added to the day (61 to 91), and a valid check digit | `198604722382` |
| `se_reservnummer` | Swedish reserve number, given in healthcare to someone without the two above | 3 to 32 characters: upper-case letters A to Z, digits and hyphens | `19860412AB27` |
| `foreign` | A personal identity number from another country | Any text of 1 to 255 characters | `120486-1234` |
| `other` | Anything else | Any text of 1 to 255 characters | `EMP-00417` |

The check digit is the last digit and follows the Luhn algorithm over the last ten digits, that
is, without the century. A number that does not fit its type is refused with `validation_failed`
and `identityNumber.number` among the `fieldErrors`. More types may be added, so accept a `type`
you do not know in a response.

An identity number is not always unique. A create or an update is normally refused with
`duplicate` when another person at the location already has the number with the same `type`.
Staff can still register one number on several persons in Cliento, on purpose. A list filtered on
that number then returns each of them. Do not rely on a `type` and `number` identifying exactly
one person.

### List persons

`GET /v1/locations/{locationId}/persons`

Scope: `customers:read`

Stability: preview

The location's persons, oldest first, one page at a time. To find one, use the filters instead of reading the whole list; "Finding a person" under Persons says how each one matches.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `email` | query | string | No | Only persons with exactly this email address, whatever its letter case. Example: `anna.lindqvist@example.com`. |
| `phoneNumber` | query | string | No | Only persons with exactly this mobile number, in E.164 format. Percent-encode the plus sign as `%2B`. Example: `+46701234567`. |
| `identityNumber` | query | string | No | Only persons with exactly this identity number, as it is stored: a Swedish personnummer is twelve digits. Example: `198604122385`. |
| `externalReference` | query | string | No | Only the person you created with this `externalReference`. Example: `crm-88121`. |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | PersonList |
| default | Any failure. See the Errors section. | Error |

### Create a person

`POST /v1/locations/{locationId}/persons`

Scope: `customers:write`

Stability: preview

Creates a person at the location. Send an `externalReference`, your own id for the person, to make the call safe to repeat. When a person at the location already has that reference, that person is returned unchanged with `200` and nothing is created. A person with an identity number that another person at the location already has, with the same `type`, is normally refused with `duplicate`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: PersonCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | A person with this `externalReference` already existed. | Person |
| 201 | The person was created. | Person |
| default | Any failure. See the Errors section. | Error |

### Get a person

`GET /v1/locations/{locationId}/persons/{personId}`

Scope: `customers:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `personId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Person |
| default | Any failure. See the Errors section. | Error |

### Get a person by id

`GET /v1/persons/{personId}`

Scope: `customers:read`

Stability: preview

The same person as under its location, found from its id. For a caller that holds an id and not the location it belongs to. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `personId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Person |
| default | Any failure. See the Errors section. | Error |

### Update a person

`PATCH /v1/locations/{locationId}/persons/{personId}`

Scope: `customers:write`

Stability: preview

Changes only the fields in the request. Either all of them are changed or none. See `PersonUpdate` for how to clear a field.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `personId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: PersonUpdate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Person |
| default | Any failure. See the Errors section. | Error |

### Delete a person

`DELETE /v1/locations/{locationId}/persons/{personId}`

Scope: `customers:write`

Stability: preview

A person with upcoming bookings cannot be deleted. Those bookings are cancelled or deleted in Cliento first.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `personId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 204 | No Content |  |
| default | Any failure. See the Errors section. | Error |

## Recipes

**Industry: hair and beauty.** The Recipes API is for hair salons. It lets an integration save the colour that was mixed for a
customer, so that staff can read it in Cliento at the next visit. It is meant for a system that
mixes or measures colour itself, such as a scale or a dispenser.

A recipe belongs to a person and describes one visit. It has one or more bowls. Each bowl lists
its components and the amount of each.

Recipes are read and added under `/v1/locations/{locationId}/persons/{personId}/recipes`. A
recipe has an id of the form `rcp_...`. Reading needs the scope `customers:read`, and adding
needs `customers:write`.

- The list is newest first, so its first recipe is what the person got the last time.
- A recipe also shows in the person's log, the notes staff keep on a person in Cliento, written
  out as text. Staff can change or remove it there.
- A recipe is not changed through the API. When the mix is adjusted, add a new one.
- A recipe moves no stock. A component can name the product it was taken from (`productId`), but
  the withdrawal is recorded through the Stock movements API. To tie the two together, put the
  `externalReference` of that stock withdrawal in the recipe's `stockReference`.
- Amounts are decimal strings, such as `"22.5"`, in the unit you send with them.
- `properties` are your own names and values, for what has no field, such as a processing time.
  They can be set on the recipe, a bowl or a component. Staff see them in Cliento, written out
  with the recipe in the person's log.
- `appData` is a JSON object for your own system's use: it is stored and returned as it is, and
  never shown in Cliento.
- A location that keeps medical records, such as a clinic, takes no new recipes: adding one there
  is refused with `not_allowed`.

### List a person's recipes

`GET /v1/locations/{locationId}/persons/{personId}/recipes`

Scope: `customers:read`

Stability: preview

The person's recipes, newest first, one page at a time. The first one is what was mixed the last time.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `personId` | path | string | Yes |  |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | RecipeList |
| default | Any failure. See the Errors section. | Error |

### Add a recipe to a person

`POST /v1/locations/{locationId}/persons/{personId}/recipes`

Scope: `customers:write`

Stability: preview

Adds a recipe to the person. It also shows in the person's log in Cliento, where staff read it. A recipe is not changed afterwards through the API: when the mix is adjusted, add a new recipe. Send an `Idempotency-Key` so that a repeat of the call does not add the recipe twice. Not available at a location that keeps medical records.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `personId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: RecipeCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 201 | Created | Recipe |
| default | Any failure. See the Errors section. | Error |

### Get a recipe

`GET /v1/locations/{locationId}/persons/{personId}/recipes/{recipeId}`

Scope: `customers:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `personId` | path | string | Yes |  |
| `recipeId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Recipe |
| default | Any failure. See the Errors section. | Error |

## Companies

The Companies API lets an integration list a location's companies, find one, and create, change
and delete them.

A company is a business customer: the owner of a vehicle, or the one that pays the invoice for a
job. The location does not communicate with a company about a booking. It communicates with a
person, such as the company's contact person.

A company belongs to one location and has an id of the form `com_...`. Reading needs the scope
`customers:read`, and writing needs `customers:write`.

- **`organisationNumber`** is the Swedish organisation number: ten digits with a valid check
  digit, with no hyphen. A number that does not fit is refused with `validation_failed`.
- **`department`** names one part of a company, such as `Used cars`, for a location that registers
  each part as a company of its own.
- **To find a company**, filter the list on `organisationNumber` or on the `externalReference` you
  gave it.
- **An organisation number is not always unique.** A create or an update is normally refused with
  `duplicate` when another company at the location already has the number, whatever its
  `department`. Staff can still register one number on several companies in Cliento, on purpose.
  That is how the parts of one company come to be several companies. Do not rely on a number
  identifying exactly one company.

### List companies

`GET /v1/locations/{locationId}/companies`

Scope: `customers:read`

Stability: preview

The location's companies, oldest first, one page at a time. To find one, filter on its organisation number or your own reference.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `organisationNumber` | query | string | No | Only companies with exactly this organisation number, ten digits. Example: `5560360793`. |
| `externalReference` | query | string | No | Only the company you created with this `externalReference`. Example: `crm-88121`. |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | CompanyList |
| default | Any failure. See the Errors section. | Error |

### Create a company

`POST /v1/locations/{locationId}/companies`

Scope: `customers:write`

Stability: preview

Creates a company at the location. Send an `externalReference`, your own id for the company, to make the call safe to repeat. When a company at the location already has that reference, that company is returned unchanged with `200` and nothing is created. A company with an organisation number that another company at the location already has is normally refused with `duplicate`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: CompanyCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | A company with this `externalReference` already existed. | Company |
| 201 | The company was created. | Company |
| default | Any failure. See the Errors section. | Error |

### Get a company

`GET /v1/locations/{locationId}/companies/{companyId}`

Scope: `customers:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `companyId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Company |
| default | Any failure. See the Errors section. | Error |

### Get a company by id

`GET /v1/companies/{companyId}`

Scope: `customers:read`

Stability: preview

The same company as under its location, found from its id. For a caller that holds an id and not the location it belongs to. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `companyId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Company |
| default | Any failure. See the Errors section. | Error |

### Update a company

`PATCH /v1/locations/{locationId}/companies/{companyId}`

Scope: `customers:write`

Stability: preview

Changes only the fields in the request. Either all of them are changed or none. See `CompanyUpdate` for how to clear a field.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `companyId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: CompanyUpdate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Company |
| default | Any failure. See the Errors section. | Error |

### Delete a company

`DELETE /v1/locations/{locationId}/companies/{companyId}`

Scope: `customers:write`

Stability: preview

A company with upcoming bookings cannot be deleted. Those bookings are cancelled or deleted in Cliento first.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `companyId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 204 | No Content |  |
| default | Any failure. See the Errors section. | Error |

## Vehicles

**Industry: automotive.** The Vehicles API is for garages, car washes and other automotive businesses. It lets an
integration list a location's vehicles, find one, and create, change and delete them.

A vehicle is what the job is done on: the car, not its owner. The owner is a person or a company.
The location communicates with the vehicle's contact person, who is a person.

A vehicle belongs to one location and has an id of the form `veh_...`. Reading needs the scope
`customers:read`, and writing needs `customers:write`.

- **`registrationNumber`** identifies the vehicle. A vehicle needs a registration number or a
  `description`; it can have both.
- **`make`, `model`, `modelYear` and `colour`** are filled in from the Swedish vehicle register
  when staff look the registration number up in Cliento. They are not set through the API and are
  left out for a vehicle that has not been looked up.
- **To find a vehicle**, filter the list on `registrationNumber` or on the `externalReference` you
  gave it.
- **A registration number is not always unique.** A create or an update is normally refused with
  `duplicate` when another vehicle at the location already has the number. Staff can still
  register one number on several vehicles in Cliento, on purpose. Do not rely on a number
  identifying exactly one vehicle.

### List vehicles

`GET /v1/locations/{locationId}/vehicles`

Scope: `customers:read`

Stability: preview

The location's vehicles, oldest first, one page at a time. To find one, filter on its registration number or your own reference.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `registrationNumber` | query | string | No | Only vehicles with exactly this registration number. Example: `ABC123`. |
| `externalReference` | query | string | No | Only the vehicle you created with this `externalReference`. Example: `crm-88121`. |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | VehicleList |
| default | Any failure. See the Errors section. | Error |

### Create a vehicle

`POST /v1/locations/{locationId}/vehicles`

Scope: `customers:write`

Stability: preview

Creates a vehicle at the location. Send an `externalReference`, your own id for the vehicle, to make the call safe to repeat. When a vehicle at the location already has that reference, that vehicle is returned unchanged with `200` and nothing is created. A vehicle with a registration number that another vehicle at the location already has is normally refused with `duplicate`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: VehicleCreate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | A vehicle with this `externalReference` already existed. | Vehicle |
| 201 | The vehicle was created. | Vehicle |
| default | Any failure. See the Errors section. | Error |

### Get a vehicle

`GET /v1/locations/{locationId}/vehicles/{vehicleId}`

Scope: `customers:read`

Stability: preview

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `vehicleId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Vehicle |
| default | Any failure. See the Errors section. | Error |

### Get a vehicle by id

`GET /v1/vehicles/{vehicleId}`

Scope: `customers:read`

Stability: preview

The same vehicle as under its location, found from its id. For a caller that holds an id and not the location it belongs to. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `vehicleId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Vehicle |
| default | Any failure. See the Errors section. | Error |

### Update a vehicle

`PATCH /v1/locations/{locationId}/vehicles/{vehicleId}`

Scope: `customers:write`

Stability: preview

Changes only the fields in the request. Either all of them are changed or none. See `VehicleUpdate` for how to clear a field.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `vehicleId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Request body**: VehicleUpdate

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Vehicle |
| default | Any failure. See the Errors section. | Error |

### Delete a vehicle

`DELETE /v1/locations/{locationId}/vehicles/{vehicleId}`

Scope: `customers:write`

Stability: preview

A vehicle with upcoming bookings cannot be deleted. Those bookings are cancelled or deleted in Cliento first.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `vehicleId` | path | string | Yes |  |
| `Idempotency-Key` | header | string | No | A value you make up for this one request, such as a UUID. Send the same value when you retry and the first answer is returned; see Retrying safely. Example: `8e03978e-40d5-43e8-bc93-6894a57f9324`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 204 | No Content |  |
| default | Any failure. See the Errors section. | Error |

## Customers

The Customers API searches persons, companies and vehicles at once. Use it when you do not know
which type of customer you are looking for. You can search by email address, by phone number, by
your own reference, or by an identifying number such as a personnummer or a vehicle registration
number.

A search returns a page of matching customers. Each match is a short entry that says which type
it is. That type's API has the full record.

The Customers API is read-only. Each type is created, changed and deleted through its own API.

- **Each entry is short**: its id, its `type`, its `locationId`, its name (for a vehicle, its
  `description`) and the number that identifies it. The id's prefix (`per_`, `com_`, `veh_`) and
  `type` say where to read the rest: Persons, Companies or Vehicles.
- **Filters** match the whole value, and several together match what has all of them: `?email=`,
  `?phoneNumber=` (in E.164 format with the plus sign percent-encoded), `?number=` (an identity
  number, an organisation number or a registration number) and `?externalReference=`. Add
  `?type=` to keep one type, and repeat it for several.
- An `externalReference` is unique among everything at a location, whatever its type.

### List customers of every type

`GET /v1/locations/{locationId}/customers`

Scope: `customers:read`

Stability: preview

The location's persons, companies and vehicles in one list of short entries, oldest first. Use it to find something when you do not know its type, for example from a phone number; then read it in full from Persons, Companies or Vehicles, as the id's prefix and `type` say.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `type` | query | array of string | No | Only these kinds: `person`, `company` or `vehicle`. Repeat the parameter for several. Example: `person`. |
| `email` | query | string | No | Only those with exactly this email address. Letter case is ignored. Example: `anna.lindqvist@example.com`. |
| `phoneNumber` | query | string | No | Only those with exactly this mobile number, in E.164 format. Percent-encode the plus sign as `%2B`. Example: `+46701234567`. |
| `number` | query | string | No | Only those with exactly this number: an identity number, an organisation number or a registration number. Example: `ABC123`. |
| `externalReference` | query | string | No | Only the one you created with this `externalReference`. Example: `crm-88121`. |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | CustomerList |
| default | Any failure. See the Errors section. | Error |

### List customers across locations

`GET /v1/customers`

Scope: `customers:read`

Stability: preview

Lists the persons, companies and vehicles of every location this API key can be used on, as short entries, one page at a time. The call works only when sharing customers between locations is turned on for the organisation in Cliento. Otherwise it is refused with `not_allowed`, and you list customers per location instead. Each entry has the `locationId` it is registered at. Read the full record, change it or delete it under that location, in Persons, Companies or Vehicles. The filters work as on a location's list.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `email` | query | string | No | Only those with exactly this email address. Letter case is ignored. Example: `anna.lindqvist@example.com`. |
| `phoneNumber` | query | string | No | Only those with exactly this mobile number, in E.164 format. Percent-encode the plus sign as `%2B`. Example: `+46701234567`. |
| `externalReference` | query | string | No | Only those you created with this `externalReference`. A reference is unique within a location, not across them, so the list can return one match per location. Example: `crm-88121`. |
| `number` | query | string | No | Only those with exactly this number: an identity number, an organisation number or a registration number. Example: `ABC123`. |
| `type` | query | array of string | No | Only these kinds: `person`, `company` or `vehicle`. Repeat the parameter for several. Example: `person`. |
| `limit` | query | integer (int32) | No | Page size, 1 to 200. Default 50. |
| `after` | query | string | No | The `nextCursor` of the previous page. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | CustomerList |
| default | Any failure. See the Errors section. | Error |

### Get a customer of any type

`GET /v1/locations/{locationId}/customers/{customerId}`

Scope: `customers:read`

Stability: preview

The short entry for a person, a company or a vehicle, by its id.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `locationId` | path | string | Yes |  |
| `customerId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Customer |
| default | Any failure. See the Errors section. | Error |

### Get a customer of any type by id

`GET /v1/customers/{customerId}`

Scope: `customers:read`

Stability: preview

The short entry for a person, a company or a vehicle, found from its id. For a caller that holds an id and not the location it belongs to. The answer carries `locationId`.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `customerId` | path | string | Yes |  |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| 200 | OK | Customer |
| default | Any failure. See the Errors section. | Error |

## Customer log

**Coming soon.** Read and add the notes and files staff keep on a person, a company or a vehicle.

## Schemas

### Address

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `city` | string or null | No | Example: `Stockholm`. |
| `country` | string or null | No | Free text; an ISO 3166-1 alpha-2 code is recommended. Example: `SE`. |
| `line1` | string or null | No | Example: `Storgatan 12`. |
| `line2` | string or null | No | Example: `Lgh 1102`. |
| `line3` | string or null | No |  |
| `postCode` | string or null | No | Example: `111 51`. |

### Communication

What the person has agreed to receive. In an update, a field you leave out keeps its value. For a new person `smsReminders`, `emailReminders` and `reviewReminders` start as true and `marketing` as false: send `marketing` as true only when the person has agreed to marketing.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `emailReminders` | boolean | No | Booking reminders by email. |
| `marketing` | boolean | No | Marketing messages. |
| `reviewReminders` | boolean | No | Requests to review a visit. |
| `smsReminders` | boolean | No | Booking reminders by SMS. |

### Company

Represents a business a location serves. Belongs to one location.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | Address | No |  |
| `createdAt` | string (date-time) | No |  |
| `department` | string | No | A part of the company, when the location serves several of them. Example: `Used cars`. |
| `email` | string | No | Example: `info@example.com`. |
| `externalReference` | string | No | Your own id for it, as sent when it was created. Example: `crm-88121`. |
| `id` | string | Yes | Example: `com_6Tn3wRd8Kq1Vb5Xs9LmPyA`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `name` | string | No | Example: `Bilfirman AB`. |
| `notes` | string | No | Staff-facing notes, at most 1000 characters. |
| `organisationNumber` | string | No | The Swedish organisation number: ten digits with a valid check digit, with no hyphen. Example: `5560360793`. |
| `otherPhoneNumber` | string | No | Example: `+4686543210`. |
| `phoneNumber` | string | No | Phone number, in E.164 format when it was set through this API. Example: `+46701234567`. |
| `updatedAt` | string (date-time) | No |  |

### CompanyCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | Address | No |  |
| `department` | string | No | A part of the company, when the location serves several of them. Example: `Used cars`. |
| `email` | string | No | Example: `info@example.com`. |
| `externalReference` | string | No | Your own id for it, at most 100 characters, such as its id in your system. It makes the create safe to repeat and lets you find it again. Compared exactly, letter case included. Example: `crm-88121`. |
| `name` | string | Yes | Example: `Bilfirman AB`. |
| `notes` | string | No | Staff-facing notes, at most 1000 characters. |
| `organisationNumber` | string | No | The Swedish organisation number: ten digits with a valid check digit, with no hyphen. Example: `5560360793`. |
| `otherPhoneNumber` | string | No | Example: `+4686543210`. |
| `phoneNumber` | string | No | Phone number in E.164 format: a plus sign, the country code and the number, at most 15 digits. Example: `+46701234567`. |

### CompanyList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Company | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `com_6Tn3wRd8Kq1Vb5Xs9LmPyA`. |

### CompanyUpdate

A JSON merge patch: send only what changes. A field you leave out keeps its value; a field set to `null` is cleared, except `name`, which cannot be cleared. `address` merges field by field.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | Address or null | No |  |
| `department` | string or null | No | A part of the company, when the location serves several of them. Example: `Used cars`. |
| `email` | string or null | No | Example: `info@example.com`. |
| `name` | string | No | Example: `Bilfirman AB`. |
| `notes` | string or null | No | Staff-facing notes, at most 1000 characters. |
| `organisationNumber` | string or null | No | The Swedish organisation number: ten digits with a valid check digit, with no hyphen. Example: `5560360793`. |
| `otherPhoneNumber` | string or null | No | Example: `+4686543210`. |
| `phoneNumber` | string or null | No | Phone number in E.164 format: a plus sign, the country code and the number, at most 15 digits. Example: `+46701234567`. |

### Customer

The short entry for a person, a company or a vehicle. The id's prefix says which type, and that type's API has the rest.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Example: `per_2QCtWVSaVO7yCRYBj1zsbA`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `name` | string | No | Example: `Anna Lindqvist`. |
| `number` | string | No | The number that identifies it, when it has one: a person's identity number, a company's organisation number or a vehicle's registration number. Example: `198604122385`. |
| `type` | string | Yes | `person`, `company` or `vehicle`. It also says where to read the rest: Persons, Companies or Vehicles. More values may be added. Example: `person`. |

### CustomerList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Customer | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `per_2QCtWVSaVO7yCRYBj1zsbA`. |

### CustomerOwner

Whose customer a person is within its location: the location's own, or that of one of the location's resources. A salon that rents out chairs uses it: a person served by a hairdresser who rents a chair is that resource's customer, and a person served by an employee is the location's. Many locations never record it, so `unassigned` is common and says nothing either way.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `resourceId` | string | No | The resource (see Resources) whose customer the person is. Present when `type` is `resource`. Example: `res_2QCtWVSaVO7yCRYBj1zsbA`. |
| `type` | string | Yes | `location`: the location's own customer. `resource`: the customer of one resource, named by `resourceId`. `unassigned`: not recorded. More values may be added. Values today: `location`, `resource`, `unassigned`. |

### Error

The body of every non-2xx response.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | string | Yes | Stable, machine-readable error code. Values today: `invalid_request`, `validation_failed`, `entity_not_found`, `duplicate`, `merchant_required`, `code_already_used`, `gift_card_already_used`, `pin_not_recognised`, `insufficient_stock`, `not_allowed`, `request_in_progress`, `idempotency_key_reused`, `unauthenticated`, `forbidden`, `account_blocked`, `rate_limited`, `route_not_found`, `method_not_allowed`, `internal_error`. |
| `details` | object of any value | No | Extra detail. For `validation_failed`, `fieldErrors` maps each rejected field to the reason. |
| `message` | string | Yes | Human-readable explanation. Not stable; do not parse it. |

### IdentityNumber

A number issued by an authority that identifies a person, such as a personnummer. The accepted format of each type is listed under Persons.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `number` | string | Yes | In the format of its type: twelve digits for a Swedish personnummer. Example: `198604122385`. |
| `type` | string | Yes | The kind of number, in lower case, such as `se_personnummer` or `foreign`. More values may be added. Example: `se_personnummer`. |

### Person

Represents an individual a location serves. Belongs to one location.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | Address | No |  |
| `communication` | Communication | Yes |  |
| `createdAt` | string (date-time) | No |  |
| `dateOfBirth` | string | No | ISO 8601 date. Example: `1986-04-12`. |
| `email` | string | No | Example: `anna.lindqvist@example.com`. |
| `externalReference` | string | No | Your own id for it, as sent when it was created. Example: `crm-88121`. |
| `gender` | string | No | One lower-case word such as `female`. More values may be added. Example: `female`. |
| `id` | string | Yes | Example: `per_2QCtWVSaVO7yCRYBj1zsbA`. |
| `identityNumber` | IdentityNumber | No |  |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `name` | string | No | Example: `Anna Lindqvist`. |
| `notes` | string | No | Staff-facing notes, at most 1000 characters. |
| `onlineBookingBlocked` | boolean | Yes | True when the person may not book online. |
| `otherPhoneNumber` | string | No | Example: `+4686543210`. |
| `owner` | CustomerOwner | Yes | Whose customer the person is within the location. See CustomerOwner. |
| `phoneNumber` | string | No | Mobile number, in E.164 format when it was set through this API. Example: `+46701234567`. |
| `updatedAt` | string (date-time) | No |  |

### PersonCreate

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | Address | No |  |
| `communication` | Communication | No |  |
| `dateOfBirth` | string | No | ISO 8601 date. Example: `1986-04-12`. |
| `email` | string | No | Example: `anna.lindqvist@example.com`. |
| `externalReference` | string | No | Your own id for it, at most 100 characters, such as its id in your system. It makes the create safe to repeat and lets you find it again. Compared exactly, letter case included. Example: `crm-88121`. |
| `gender` | string | No | Values today: `male`, `female`, `other`. |
| `identityNumber` | IdentityNumber | No |  |
| `name` | string | Yes | Example: `Anna Lindqvist`. |
| `notes` | string | No | Staff-facing notes, at most 1000 characters. |
| `onlineBookingBlocked` | boolean | No | Set to true to stop the person booking online. |
| `otherPhoneNumber` | string | No | Example: `+4686543210`. |
| `phoneNumber` | string | No | Mobile number in E.164 format: a plus sign, the country code and the number, at most 15 digits. Example: `+46701234567`. |

### PersonList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Person | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `per_2QCtWVSaVO7yCRYBj1zsbA`. |

### PersonUpdate

A JSON merge patch: send only what changes. A field you leave out keeps its value; a text field, `identityNumber` or `address` set to `null` is cleared, except `name`, which cannot be cleared. `address` merges field by field; `identityNumber` is replaced as a whole. The booleans (`communication.*`, `onlineBookingBlocked`) are true or false and cannot be null.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `address` | Address or null | No |  |
| `communication` | Communication | No |  |
| `dateOfBirth` | string or null | No | ISO 8601 date. Example: `1986-04-12`. |
| `email` | string or null | No | Example: `anna.lindqvist@example.com`. |
| `gender` | string or null | No | Values today: `male`, `female`, `other`, `null`. |
| `identityNumber` | IdentityNumber or null | No |  |
| `name` | string | No | Example: `Anna Lindqvist`. |
| `notes` | string or null | No | Staff-facing notes, at most 1000 characters. |
| `onlineBookingBlocked` | boolean | No | Set to true to stop the person booking online. |
| `otherPhoneNumber` | string or null | No | Example: `+4686543210`. |
| `phoneNumber` | string or null | No | Mobile number in E.164 format: a plus sign, the country code and the number, at most 15 digits. Example: `+46701234567`. |

### Recipe

A record of what was mixed for a person at one visit. Belongs to that person.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `appData` | object of any value | No | The JSON object of your own that is stored with the recipe, returned as it is. Example: `{"formulaId":"F-2291"}`. |
| `bowls` | array of RecipeBowl | Yes | The bowls mixed, in order. |
| `createdAt` | string (date-time) | Yes | Example: `2026-10-11T09:24:31Z`. |
| `id` | string | Yes | Example: `rcp_4Kd8sWn2Qy6Hb9Tf3LmVxA`. |
| `note` | string | No | Example: `Slightly warmer than last time.`. |
| `personId` | string | Yes | Example: `per_6Jm2tRx8Lc4Nq7Bv5ZgWkD`. |
| `properties` | object of string | No | Example: `{"processingTime":"35 min"}`. |
| `stockReference` | string | No | Example: `treatment-2026-10-11-0042`. |
| `title` | string | No | Example: `Colour, roots and lengths`. |
| `updatedAt` | string (date-time) | No | When staff last changed the recipe in Cliento. Left out when they never have. Example: `2026-10-12T14:02:10Z`. |

### RecipeBowl

One bowl: what was mixed together, usually colour and a developer.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `components` | array of RecipeComponent | Yes | What went into the bowl, 1 to 20. |
| `properties` | object of string | No | Your own names and values for this bowl, with the same limits as a component's `properties`. Example: `{"processingTime":"35 min"}`. |
| `ratio` | string | No | Parts of colour to parts of developer. Example: `1:2`. |
| `title` | string | No | What the bowl is for. At most 200 characters. Example: `Roots`. |

### RecipeComponent

One thing that went into a bowl.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | string | Yes | How much, in `unit`, as a decimal string greater than 0 with at most three decimals. Example: `30`. |
| `name` | string | Yes | What it is, as the hairdresser reads it. At most 200 characters. Example: `Koleston Perfect 7/0`. |
| `productId` | string | No | The product the component was taken from, when it is one of the location's. Example: `prd_5Tg8mPq1Xc7Jd2Ls9WbNrK`. |
| `properties` | object of string | No | Your own names and values for this component, at most 30. A name is at most 100 characters and a value at most 500. Example: `{"brand":"Wella"}`. |
| `role` | string | No | What the component is in the mix, as one lower-case word such as `colour` or `developer`. The list is open, so accept a value you do not know. Example: `colour`. |
| `shade` | string | No | The shade code of a colour. At most 50 characters. Example: `7/0`. |
| `strengthPercent` | string | No | The strength of a developer in per cent, as a decimal string greater than 0 and at most 100, with at most two decimals. Example: `6`. |
| `unit` | string | Yes | The unit of `amount`, as the hairdresser reads it. At most 20 characters. Example: `g`. |

### RecipeCreate

What was mixed for a person at one visit.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `appData` | object of any value | No | A JSON object of your own, for what the recipe has no field for. It is stored and returned as it is, and never shown in Cliento. At most 16 kB. A number keeps the precision of a double; send a longer one as a string. Example: `{"formulaId":"F-2291"}`. |
| `bowls` | array of RecipeBowl | Yes | The bowls mixed, 1 to 20, in order. |
| `note` | string | No | Free text. At most 4000 characters. Example: `Slightly warmer than last time.`. |
| `properties` | object of string | No | Your own names and values for the whole recipe, with the same limits as a component's `properties`. Example: `{"processingTime":"35 min"}`. |
| `staffId` | string | No | The staff member who made the recipe, shown as its author in the person's log. Without it the author is the API key. Example: `stf_7Hn3kQw9Zr4Tb6Vy1MxPdE`. |
| `stockReference` | string | No | The `externalReference` of the stock document that took the recipe's products out of stock, when you recorded one. At most 100 characters. Example: `treatment-2026-10-11-0042`. |
| `title` | string | No | What the recipe is for. At most 200 characters. Example: `Colour, roots and lengths`. |

### RecipeList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Recipe | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `rcp_4Kd8sWn2Qy6Hb9Tf3LmVxA`. |

### Vehicle

Represents a vehicle a location works on. Belongs to one location.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `colour` | string | No | From the vehicle register. Example: `Red`. |
| `createdAt` | string (date-time) | No |  |
| `description` | string | No | What the vehicle is called in Cliento. A lookup in the vehicle register fills it in with the make, model, colour and year; staff can change it. Example: `The red van`. |
| `externalReference` | string | No | Your own id for it, as sent when it was created. Example: `crm-88121`. |
| `id` | string | Yes | Example: `veh_3Ks7pQx2Wn9Tb4Vd6HyRcE`. |
| `locationId` | string | Yes | Example: `loc_4fK9xQ2mLr8TzB1vNc7HdW`. |
| `make` | string | No | From the Swedish vehicle register, when staff have looked the vehicle up in Cliento. Not set through the API. Example: `Volvo`. |
| `model` | string | No | From the vehicle register. Example: `V60`. |
| `modelYear` | integer (int32) | No | From the vehicle register. Example: `2021`. |
| `notes` | string | No | Staff-facing notes, at most 1000 characters. |
| `registrationNumber` | string | No | The registration number, one to seven characters. Example: `ABC123`. |
| `updatedAt` | string (date-time) | No |  |

### VehicleCreate

A vehicle needs a `registrationNumber` or a `description`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string | No | What the vehicle is called in Cliento, such as `The red van`. Example: `The red van`. |
| `externalReference` | string | No | Your own id for it, at most 100 characters, such as its id in your system. It makes the create safe to repeat and lets you find it again. Compared exactly, letter case included. Example: `crm-88121`. |
| `notes` | string | No | Staff-facing notes, at most 1000 characters. |
| `registrationNumber` | string | No | The registration number, one to seven characters. Example: `ABC123`. |

### VehicleList

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of Vehicle | Yes |  |
| `nextCursor` | string | No | Pass as `after` to fetch the next page. Absent on the last page. Example: `veh_3Ks7pQx2Wn9Tb4Vd6HyRcE`. |

### VehicleUpdate

A JSON merge patch: send only what changes. A field you leave out keeps its value; a field set to `null` is cleared, but a vehicle must keep a `registrationNumber` or a `description`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string or null | No | What the vehicle is called in Cliento, such as `The red van`. Example: `The red van`. |
| `notes` | string or null | No | Staff-facing notes, at most 1000 characters. |
| `registrationNumber` | string or null | No | The registration number, one to seven characters. Example: `ABC123`. |
