Authentication

The WalletPlatform API uses the OAuth 2.0 Client Credentials flow. Every request (except /auth/token and /.well-known/*) requires a Bearer JWT.

Get a Token

POST /api/v1/auth/token

Request body

FieldTypeDescription
tenant_id UUID required Your tenant identifier.
application_id UUID required The application requesting the token. Must belong to the tenant and be Active.
application_secret string required Plaintext secret. Server-side verification is Argon2id with per-row salt.
grant_type string required Must be exactly client_credentials.

Response

โ— 200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type":   "Bearer",
  "expires_in":   3599,
  "expires_at":   1747654800,
  "scope":        "passes:read passes:write templates:read messages:read messages:write webhooks:write analytics:read"
}

Scopes

The scopes embedded in the JWT depend on the application's AppType:

ScopeGrants
passes:readGET /passes, /passes/{id}, /passes/{id}/versions
passes:writePOST /passes, PATCH /passes/{id}, DELETE /passes/{id}, /batches
templates:readGET /templates*
templates:write(reserved for future write endpoints)
messages:readGET /messages*, /passes/{id}/messages
messages:writePOST /messages
webhooks:writeAll /webhooks endpoints
analytics:readGET /analytics/events, /analytics/summary

Using the token

Attach the JWT as a Bearer header on every request:

curl https://api.walletplatform.io/api/v1/passes \
  -H "Authorization: Bearer $TOKEN"
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);
var passes = await client.GetFromJsonAsync<JsonElement>(
    "https://api.walletplatform.io/api/v1/passes");
const r = await fetch('https://api.walletplatform.io/api/v1/passes', {
  headers: { 'Authorization': `Bearer ${token}` }
});
r = requests.get(
    'https://api.walletplatform.io/api/v1/passes',
    headers={'Authorization': f'Bearer {token}'},
)
$ctx = stream_context_create(['http' => [
    'header' => "Authorization: Bearer $token",
]]);
$body = file_get_contents('https://api.walletplatform.io/api/v1/passes', false, $ctx);

Token lifetime

Tokens are valid for 60 minutes. Re-request a token before or after expiry โ€” there is no refresh-token flow; client-credentials is designed to be repeated cheaply (Argon2id verification ~200ms server-side).

Common errors

HTTPerrorCause
400invalid_requestMissing/malformed field (e.g. wrong grant_type).
401invalid_clientTenant/Application not found, secret mismatch, or tenant inactive.
403unauthorized_clientApplication is disabled.
403insufficient_scopeJWT does not have the scope required for the route.
429too_many_requestsToken endpoint rate-limit (10/minute per IP).

JWKS (verify tokens client-side)

Public keys for offline JWT verification are exposed at GET /.well-known/jwks.json. Cache-control headers indicate a 24-hour TTL โ€” refresh on signature mismatch only.