# Authentication

The API uses OAuth2 **`client_credentials`** — the machine-to-machine grant. There is no user, no browser redirect, and no consent screen.

Two hosts are involved, and your client talks to both:

| Role | Host | Purpose |
|---|---|---|
| Identity provider | `https://auth.shiftavo.com` | issues tokens |
| Shard | from your token's `api_base` claim | serves the data |

## Presenting your credentials

Both standard OAuth2 client-authentication methods (RFC 6749 §2.3.1) are accepted. Use whichever your HTTP or OAuth library makes easy — they are equivalent here.

`client_secret_post` — credentials in the form body:

```http
POST /identity/o/api/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
client_id=cust-acme-integration
client_secret=<secret>
scope=shifts.read
```

`client_secret_basic` — credentials in an HTTP Basic header, only `client_id` in the body:

```http
POST /identity/o/api/token HTTP/1.1
Authorization: Basic base64("cust-acme-integration:<secret>")
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
client_id=cust-acme-integration
scope=shifts.read
```

`client_secret_basic` is the spec's default and what most off-the-shelf OAuth libraries emit; `client_secret_post` is simpler to construct by hand, with no base64 step.

## Expiry and refresh

Tokens default to **1800 seconds**. The `client_credentials` grant has **no refresh token** — by design (RFC 6749 §4.4.3), and it costs you nothing: your client already holds the `client_id` and `client_secret`, so "refreshing" is just asking for a new token with the same call. A refresh token would only be one more secret to store.

Two rules make a client robust:

- **Cache** the token and reuse it until close to `expires_in`. Refreshing ~30 seconds early avoids edge-of-expiry races.
- **On a `401`** from the data API — a token can be invalidated early, for instance by a key rotation — drop the cached token, fetch a fresh one, and **retry the request once**.

## Routing: the `api_base` claim

The token is a JWT. Beyond the standard OAuth fields it carries:

| Claim | Example | Meaning |
|---|---|---|
| `api_base` | `https://app.shiftavo.com` | **Base URL of the host to call** |
| `aud` | `["https://app.shiftavo.com/api"]` | Target shard API |
| `tenant_id` | `0194d1c0-…` | The company you are bound to |
| `tenant_name` | `Acme` | Display name |
| `tenant_status` | `active` | Non-active companies are refused a token |

Read `api_base` and call that host. Keep a configured fallback for the case where the claim is absent, but prefer the claim: it is what lets us add or move a shard without any change on your side.

`aud` and the company binding are set **server-side** from the tenant your credentials belong to. They are not requestable — a client physically cannot target another company or another shard.

## What a machine token is not

Machine tokens have **no `sub`** and no user or membership claims. Every call acts as the integration itself, limited by its scopes. Two consequences:

- The API cannot infer who is accountable for a write, so some writes make you name a person explicitly — see {doc}`authorizing-manager`.
- A first-party user token (from the web or mobile app) is **rejected** on this surface. It is machine-only.

## Errors at this stage

| Symptom | Cause |
|---|---|
| `401` from the token endpoint | wrong `client_id` / `client_secret`, or the company is suspended |
| `400 invalid_scope` at the token endpoint | you asked for a scope that was not granted to your client |
| `401` from the data API | expired, invalid, or early-invalidated token — refresh and retry once |
| `403 insufficient_scope` from the data API | valid token, missing grant — see {doc}`scopes` |
