Obtaining tokens
This page is the wire-level contract for the OAuth token endpoint.
The token endpoint URL in the samples below — https://auth.<environment>/oauth/token
— is a placeholder. Use the real URL delivered to you at onboarding.
Token request
POST /oauth/token HTTP/1.1
Host: auth.<environment>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=screenings.read
| Parameter | Required | Notes |
|---|---|---|
grant_type | yes | Must be client_credentials. |
client_id | yes | Issued at onboarding. |
client_secret | yes | Issued at onboarding. Bearer-equivalent — protect it. |
scope | yes | The space-separated scopes you need. The only scope offered in v1 is screenings.read. |
The endpoint conforms to RFC 6749 §4.4.
Token response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "screenings.read"
}
| Field | Type | Notes |
|---|---|---|
access_token | string (JWT) | The bearer token. Send it on every call to the REST API. |
token_type | string | Always Bearer. |
expires_in | integer | Seconds until the token expires. Currently 3600 (1 hour). |
scope | string | The scopes actually granted. May be a subset of what you requested. |
JWT claims
Although the token is opaque from the API's point of view, it is deliberately a JWT so that you can decode it for diagnostics. The following claims are present and stable:
| Claim | Type | Meaning |
|---|---|---|
iss | string | Issuer. The token endpoint's base URL. |
aud | string | Audience. The Listo REST API's identifier. |
sub | string | Subject. Typically equal to your client_id. |
iat | integer (epoch s) | Issued-at timestamp. |
exp | integer (epoch s) | Expiry timestamp. |
scope | string | The granted scopes, space-separated. |
tenant_id | string (UUID) | The single TenantId this token is scoped to. Each (client_id, client_secret) pair is provisioned for exactly one tenant; the minted token inherits that scope. The Sessions API requires the {tenantId} in the request path to equal this claim — a different value returns 403 forbidden. To query a different tenant, mint a token using that tenant's credentials. |
Refresh strategy
Tokens are short-lived (1 hour). Recommended pattern:
- Cache the token in process memory. A single in-process variable is enough for most consumers; a small module-level singleton is fine.
- Reuse it until 60 seconds before
exp. Compute the deadline once when you receive the token (expiresAt = now + (expires_in - 60) s). - Re-mint, don't refresh. There is no refresh token. When your cached
token is close to expiring, request a fresh one with the same
client_credentialsrequest. - Don't mint a token per API call. It works, but it's a free round-trip you didn't have to pay for, and it creates avoidable load on the token endpoint.
Using the token
Send the token in the standard Authorization header on every REST call:
GET /v1/tenants/0d3e2b51-4e7a-44b6-8d5e-1c3a9b2f7c40/screenings?from=2026-05-08T00:00:00Z&to=2026-05-08T01:00:00Z HTTP/1.1
Host: api.<environment>
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Accept: application/json
The {tenantId} segment must equal the token's single tenant_id
claim. A different value returns 403 forbidden. To query a different
tenant, mint a token using that tenant's (client_id, client_secret)
pair.
If the token has expired, you'll get a 401 Unauthorized with the
unauthenticated error code. Treat 401 as a signal to re-mint and
retry exactly once.
Token-endpoint errors
The token endpoint follows
RFC 6749 §5.2
for error responses. Most errors return 400 Bad Request; the exception is
invalid_client, which returns 401 Unauthorized (with a WWW-Authenticate
header) per the same section:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "invalid_client",
"error_description": "Client authentication failed."
}
error | HTTP | When |
|---|---|---|
invalid_request | 400 | Required parameter missing, malformed, or duplicated. |
invalid_client | 401 | client_id unknown or client_secret wrong. |
unauthorized_client | 400 | The client is recognized but not allowed to use this grant. |
invalid_grant | 400 | The supplied grant cannot be used. |
unsupported_grant_type | 400 | grant_type is not client_credentials. |
invalid_scope | 400 | The requested scope is not granted to your client. |
Where to next
- REST reference — the endpoints you'll call with this token.