Skip to main content

Create a Token

This page describes how to obtain an OAuth 2.0 access token (and refresh token) from the integration service. The endpoint follows the OAuth 2.0 token endpoint contractexpires_in is a lifetime in seconds, not an absolute Unix timestamp.

Endpoint

POST /v2/oauth/token

The endpoint is public at the gateway (no Bearer required) but the integration backend still validates the client credentials and, when configured, the client mTLS certificate / IP allowlist (see Create Client).

Supported Grant Types

Grant TypeRequired ParametersPurpose
passwordclient_id, client_secret, username, passwordInteractive supervisor login. Returns a supervisor-scoped access token (15 min) and refresh token (30 days).
client_credentialsclient_id, client_secretMachine-to-machine. Returns a client-scoped access/refresh token pair whose lifetime is controlled by the client's token_expiry policy.
refresh_tokenclient_id, client_secret, refresh_tokenExchanges a valid refresh token for a new access/refresh pair.

The client must have been issued the requested grant type in its grant_types array; the server rejects unknown or unassigned grants with invalid_request.

Password Grant

Request

curl --request POST \
--url \{\{baseUrl\}\}/v2/oauth/token \
--header 'Content-Type: application/json' \
--data '{
"client_id": "c1a9beda-b14c-4eb5-b880-113023e6fc2d",
"client_secret": "01JNVVQ1X3XYRJYX4BJ7Y2QM9K",
"grant_type": "password",
"username": "uber@apex.com",
"password": "Apex1234!"
}'

The supervisor identified by username must belong to the same node as the OAuth client — the password grant is rejected across tenants even when credentials match.

Token Lifetimes (Password Grant)

TokenLifetimeNotes
Access15 minutesCarries exp claim (RFC 7519 §4.1.4).
Refresh30 daysCarries iat so the absolute max-age guard can fire (see below).

Client-Credentials Grant

Request

curl --request POST \
--url \{\{baseUrl\}\}/v2/oauth/token \
--header 'Content-Type: application/json' \
--data '{
"client_id": "c1a9beda-b14c-4eb5-b880-113023e6fc2d",
"client_secret": "01JNVVQ1X3XYRJYX4BJ7Y2QM9K",
"grant_type": "client_credentials"
}'

The lifetime is taken from the client's token_expiry policy (enabled + duration). When enabled = false the access token is issued without an exp claim and only the refresh-token max-age cap (see below) protects against indefinite reuse.

Refresh Grant

curl --request POST \
--url \{\{baseUrl\}\}/v2/oauth/token \
--header 'Content-Type: application/json' \
--data '{
"client_id": "c1a9beda-b14c-4eb5-b880-113023e6fc2d",
"client_secret": "01JNVVQ1X3XYRJYX4BJ7Y2QM9K",
"grant_type": "refresh_token",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'

A refresh-token call always issues a fresh access and refresh token — store both from the response.

Refresh-Token Max-Age Cap

Independent of the per-client token_expiry policy, every refresh token issued by the integration is hard-capped at 365 days from iat. After that, the refresh exchange fails with invalid_request even if the JWT itself has no exp claim. This is a defense-in-depth ceiling that prevents leaked refresh tokens from being usable forever when token_expiry.enabled = false.

Refresh tokens issued before this guard was rolled out (no iat claim) are grandfathered through one rotation cycle.

Response

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ8.",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.",
"expires_in": 900,
"token_type": "Bearer",
"scope": "public",
"created_at": 1741603326
}

Response Fields

FieldTypeDescription
access_tokenstringJWT to be sent as Authorization: Bearer … on subsequent calls.
refresh_tokenstringJWT redeemable on this same endpoint via the refresh_token grant.
expires_innumberAccess-token lifetime in seconds (OAuth 2.0 §5.1). For the password grant this is 900 (15 min). For the client-credentials grant it is the token_expiry.duration configured on the client. The JWT itself still carries the absolute exp claim per RFC 7519.
token_typestringAlways Bearer.
scopestringSpace-separated permission list (password grant: role permissions; client-credentials: configured scopes).
created_atnumberUnix timestamp the token was issued at.
Breaking change vs. earlier docs

Earlier revisions of this page documented expires_in as an absolute Unix timestamp (e.g. 1741604226). That was incorrect and contradicted OAuth 2.0 §5.1. The integration service now returns it as a lifetime in seconds. Clients that subtracted created_at from expires_in to compute the lifetime will over-rotate; update them to use expires_in directly.

Using the Token

Include the access token on every protected call:

curl --request GET \
--url \{\{baseUrl\}\}/v2/admin/tokens/68aeb272d9336c619ce249f6 \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ8.'

Login-issued tokens are additionally validated against a Redis cache keyed by user sub + node aud — invalidate sessions by removing that entry to force re-authentication.

Error Responses

StatusBody codeCause
400invalid_requestMissing or malformed parameters (no username on password grant, no refresh_token on refresh grant, etc.).
401invalid_requestWrong client_id / client_secret, mismatched supervisor password, refresh token expired or past the 365-day max-age, supervisor on a different node than the client.
403invalid_requestClient grant_types does not include the requested grant.
403ip_not_allowedCaller IP is not in the client's allowed_ips.
403mtls_requiredClient has require_certificate = true but the request did not present a valid client certificate.
429rate_limit_exceededPer-second / minute / hour ceiling hit. Honour the Retry-After header.

Errors share the integration's standard envelope:

{
"code": -1,
"domain": 0,
"message": "invalid_request"
}