# Cliento API reference

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

The Cliento API gives your own systems server-to-server access to your organisation's data in
Cliento.

Everything in the API belongs to a location. A location is one place of business, such as one
salon or one branch of a car wash. Start with `GET /v1/locations` to list the locations your key
can use. Almost every other call takes a location id in its path. An item can also be read by its
id, without its location; see "Identifiers".

This introduction covers what applies to every call. Each part of the reference describes its own
API.

## Industry-specific APIs

Most APIs are used by every organisation, whatever its industry. A few are made for one industry,
such as the Recipes API for hair and beauty and the Vehicles API for automotive. The reference
labels such an API with its industry. It can label a single field or value in the same way. The
label is information only: the API does not check an organisation's industry, and no call is
refused because of it.

## Authentication

Create an API key for your organisation and send it on every request:

```
Authorization: Bearer cl_live_...
```

A key belongs to one organisation and is shown only once, when it is created. Keep it on your
server; never ship it in a browser or mobile app. If a key leaks, revoke it and create a new one.

Each key carries **scopes** that decide what it may do, for example `customers:read` and
`customers:write`. A call outside the key's scopes is answered with `403` and code `forbidden`.

## Identifiers

Every id is an opaque string with a prefix that says what it identifies. Store ids as strings and
do not parse them: their length and the characters after the prefix may change. An id from one host, such
as our staging host, does not exist on another.

An id is enough to read the item it identifies. Every item has a path under its location, such as
`/v1/locations/{locationId}/persons/{personId}`, and one without it, `/v1/persons/{personId}`.
Use the second when you have an id and not its location: an id you stored, or one that another
item refers to. It works for an item at any location your key can use, and the answer includes
the item's `locationId`. An id at a location your key cannot use is answered like an id that does
not exist, with `entity_not_found`.

| Prefix | Identifies | Example |
|---|---|---|
| `loc_` | A location | `loc_4fK9xQ2mLr8TzB1vNc7HdW` |
| `per_` | A person | `per_2QCtWVSaVO7yCRYBj1zsbA` |
| `com_` | A company | `com_6Tn3wRd8Kq1Vb5Xs9LmPyA` |
| `veh_` | A vehicle | `veh_3Ks7pQx2Wn9Tb4Vd6HyRcE` |
| `stf_` | A staff member | `stf_7Qm2xLd9Kc4Vb1Ts8HpWzE` |
| `mer_` | A merchant | `mer_2Rk8sVh3Yq6Nf1Tz0JwUcM` |
| `gft_` | A gift card | `gft_7Qm2xN0cT1kPz4Wb9LrEaD` |
| `prd_` | A product | `prd_5Tg8mPq1Xc7Jd2Ls9WbNrK` |
| `mov_` | A stock movement | `mov_9Bd2fHk4Mn7Pq1Sv6XzCtA` |
| `res_` | A resource | `res_2QCtWVSaVO7yCRYBj1zsbA` |
| `rgr_` | A resource group | `rgr_5Jk9wQe2Ty6Nb3Vx8MzLcA` |
| `svc_` | A service | `svc_3Gk8rTn5Ym1Wc7Zp2LdQxB` |
| `sgr_` | A service group | `sgr_8Vc2nXd5Rt1Kq7Lm4HwYpZ` |
| `rcp_` | A recipe | `rcp_4Kd8sWn2Qy6Hb9Tf3LmVxA` |
| `bkg_` | A booking | `bkg_5Rt8mKq2Wn7Xb4Vd9HyLcA` |
| `bks_` | A series of bookings | `bks_6Wq2nYd8Rk4Tc9Xb3HmLzP` |
| `sal_` | A sale | `sal_8Hd3kWq5Zn2Tb7Xc4LmPyR` |

## Created and updated times

Persons, companies, vehicles, resources, services, products, gift cards and recipes have
`createdAt` and `updatedAt`, both in UTC.

`updatedAt` is the time the stored item last changed. Use it to tell whether your copy of an item
is current. `createdAt` is the time the item was created. It is left out where it is not known,
such as for a service created before these times were kept.

A service's `updatedAt` also moves when one of its resource services is added, changed or
removed, because a service is read with them.

Three things do not move `updatedAt`:

- A value that changes with time alone, such as a gift card's `status` when the card expires.
- A change to the location's own settings, such as its time zone.
- A barcode that a supplier adds to the catalogue article a product is linked to.

## Live and test keys

A key that starts with `cl_live_` belongs to an organisation with real data, in production. Every
other key starts with `cl_test_`. In production a `cl_test_` key belongs to a sandbox: an
organisation of its own on the same API and the same host, with data that is yours to create and
throw away. A sandbox sends no SMS or email to customers
and takes no payments. Outside production, such as on our staging host, all keys are `cl_test_`.

## Updating

Updates use `PATCH` and change only what you send. A field you leave out keeps its value. To
clear a field, send it as `null`. One request is applied as a whole or not at all.

## Paging

Most lists are paged. Pass the `nextCursor` of one page as `after` to get the next, and go on
until a page has none. Follow `nextCursor` even on a list that fits in one page today. Locations,
staff and merchants are returned in one response.

A cursor names the last item you read. Some lists refuse a cursor whose item has been removed
since, with `validation_failed` on `after`. Read the list again from the start when that
happens.

## Errors

Every non-2xx response has the same JSON body:

```json
{ "code": "validation_failed", "message": "...", "details": { "fieldErrors": { "email": "..." } } }
```

Branch on `code`; `message` is for humans and may change. An id that does not exist, or that your
key may not access, is answered with `400` and code `entity_not_found`. `404` is reserved for a
URL that is not an endpoint at all.

Requests are validated strictly: an unknown JSON field is rejected with `invalid_request` rather
than ignored.

## Rate limits

Requests are limited per API key. When the limit is exceeded the answer is `429` with code
`rate_limited` and a `Retry-After` header giving the number of seconds to wait.

A request body may be at most 1 MB, far more than any call needs. A larger one is answered with
`413` and code `invalid_request`.

## Versioning

The version is in the URL (`/v1`). Every operation in the reference is marked **stable** or
**preview**.

- A **stable** operation only changes in backwards-compatible ways: new optional request fields,
  new response fields and new values in open-ended enumerations. Your client must tolerate
  response fields and enumeration values it does not know. A breaking change to a stable
  operation means a new version, and the previous version keeps working during a published
  deprecation period.
- A **preview** operation works and can be built against, but its fields, paths and answers may
  still change without a new version. Tell us when you build against one, so that we know to
  make it stable.

New operations can be added at any time.

## Amounts

Money is a decimal string in the location's currency, such as `"500.00"`, never a JSON number.
Send at most two decimals.

## Retrying safely

A timeout or a dropped connection can hide the answer to a request that did succeed. Every call
that changes something can be made safe to send again.

**`Idempotency-Key`** is the general way. It is a header that every `POST`, `PATCH` and `DELETE`
that changes something accepts, and it holds a value you make up for one request, such as a UUID.
Send the same value when you retry. The first request runs. For 24 hours every repeat is answered
with the first request's answer, marked with the header `Idempotent-Replayed: true`, and does not
run again. A value belongs to the API key it was sent with: the same value sent with another key
is a different request. Send one on every write.

- Using an `Idempotency-Key` for a different request (another path or body) is refused with `422`
  and `idempotency_key_reused`.
- A repeat that arrives while the first request is still running is refused with `409` and
  `request_in_progress`; wait a moment and send it again.
- An answer of `500` or above is not kept, so a retry runs the request again.
- A refusal such as `validation_failed` is kept like any other answer: correct the request and
  send it with a new `Idempotency-Key`.

**`externalReference`** is a second way, for a call that creates something. It is your own id for
what you create, such as an order number or the id in your own system, sent in the body. When a
call arrives with a reference that is already in use, nothing is created or changed: the existing
item is returned as it is, with `200` instead of `201`. A reference that belongs to a customer
of another type is refused with `duplicate` instead. Unlike an `Idempotency-Key`, a reference
lasts as long as the item does, and you can find the item by it later. Persons, companies,
vehicles, gift cards and stock documents (the stock changes recorded in one call) take one. When
you have an id of your own, send it as well as the header.
