Skip to content

Mint an access token

POST
/oauth/token
curl --request POST \
--url https://api.aerandil.com/oauth/token \
--header 'Content-Type: application/json' \
--data '{ "client_id": "example", "client_secret": "example", "grant_type": "client_credentials", "scope": "example" }'

OAuth 2.1 client_credentials grant — exchange a client_id + client_secret for a short-lived bearer token whose scopes claim narrows the issued token to a subset of the client’s pre-provisioned scopes.

Deviation from RFC 6749 §4.4.2. Aerandil accepts the grant parameters as a JSON body (application/json), not the application/x-www-form-urlencoded body the RFC mandates. This is an explicit operational choice — every other Aerandil endpoint takes JSON, and SDK generators emit cleaner code for one wire format than two. Modern SaaS APIs (Stripe, Auth0 management API) have made the same call. RFC-conformant integrations generally fail open against a JSON token endpoint; if they don’t, the integrator wraps a per-PMS shim.

Public endpoint — no bearer required. The HTTPS transport is the credential carrier; the request body is intentionally not authenticated by a prior token (that would be a chicken-and-egg).

Media typeapplication/json

OAuth 2.1 client_credentials grant body. JSON-encoded, not form-urlencoded — see /oauth/token’s endpoint description for the rationale.

object
client_id
required
string
client_secret
required

Verified against the client’s argon2id-hashed secret.

string
grant_type
required

Aerandil supports only client_credentials in v1. Any other value yields 422 unsupported_grant_type.

Allowed value: client_credentials
scope

Optional space-separated scope filter. When present, the issued token’s scope is the intersection of the filter and the client’s pre-provisioned scopes — a way to narrow per-request without rotating the client’s permanent grant.

string

Token minted. scope is the granted set — intersection of the client’s pre-provisioned scopes with the scope request parameter (when present).

Media typeapplication/json
object
access_token
required

Short-lived HS256 JWT. Send as Authorization: Bearer <token> on subsequent calls.

string
expires_in
required

Token lifetime in seconds. Default 3600 (one hour); deployment-configurable via AERANDIL_TOKEN_TTL_SECS.

integer
>= 1
scope
required

Space-separated list of granted scopes. The set the token actually carries — already narrowed by any scope request filter and the client’s pre-provisioned grants.

string
token_type
required

Always Bearer in v1.

Allowed value: Bearer
Example
{
"token_type": "Bearer"
}

Authentication failed. Returned for unknown client_id and for client_secret mismatch alike — the two failure modes are deliberately indistinguishable to prevent client_id enumeration.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}

Request body invalid — unsupported grant_type (only client_credentials is accepted), malformed JSON, or unknown body fields.

Media typeapplication/json

Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.

object
error
required
object
code
required

Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).

string
instance_id

Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.

string
message
required

Human-readable explanation of the failure.

string
parameters

Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).

Examplegenerated
{
"error": {
"code": "example",
"instance_id": "example",
"message": "example",
"parameters": "example"
}
}