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:

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 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 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

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 Scopes