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 |
|
issues tokens |
Shard |
from your token’s |
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:
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:
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
401from 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 |
|---|---|---|
|
|
Base URL of the host to call |
|
|
Target shard API |
|
|
The company you are bound to |
|
|
Display name |
|
|
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 Naming the human behind a write.
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 |
|---|---|
|
wrong |
|
you asked for a scope that was not granted to your client |
|
expired, invalid, or early-invalidated token — refresh and retry once |
|
valid token, missing grant — see Scopes |