Exchange a PAT for a JWT
A personal access token is the long-lived credential, but the
API doesn't accept it
directly — every call needs a short-lived JWT in the
Authorization header. Trade the PAT for a JWT
once via the OAuth pat_exchange grant, use the
JWT until it expires, then exchange again.
Why a two-step?
Scope is enforced on every API call, and the cheapest way to do that is to read it from a signed JWT — no database lookup, no per-request validation against a long-lived PAT registry. The PAT stays in the caller's secret store; only the short-lived JWT travels on the wire.
The request shape
Post to the API's token endpoint with the
pat_exchange grant type:
curl -X POST https://api.airbrx.ai/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "pat_exchange",
"pat": "<your-pat-secret>"
}'
The response is a standard token response:
{
"access_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 3600
}
Use access_token as a bearer token on every
subsequent API call until it expires.
What the JWT inherits
The JWT carries the same four-axis scope cube the PAT had — same method, path, tenant, and account constraints — plus any plan-level restrictions from the underlying account. The exchange is an issuing operation, not a privilege escalation; the JWT can never do more than the PAT could.
Inspecting a PAT without exchanging it
To sanity-check what a PAT grants — its scope, expiry, and the tenants and accounts it can touch — before wiring it into a long-running service, post it to the introspection endpoint instead of the token endpoint:
curl -X POST https://api.airbrx.ai/oauth/introspect \
-H "Content-Type: application/json" \
-d '{ "token": "<your-pat-secret>" }'
The response is the standard RFC 7662 shape:
active, exp, scope,
tenants, accounts. Same
wire-exposure as the exchange — the PAT goes in the JSON
body, never in the Authorization header or URL
— but no new credential is issued, so there's no fresh JWT
to manage afterwards.
Handling expiry
Two patterns:
Proactive refresh
Track expires_in at exchange time, and re-exchange
a minute or two before it lapses. The right pattern for
services that run continuously.
Reactive refresh
Use the JWT until you get a 401 back, then exchange for a new one and retry the failed request once. Less efficient under sustained load, simpler to reason about for occasional scripts.
Either pattern: hold the PAT secret in the parent process's memory or secret store; don't write the issued JWTs to disk. Pipelines that fork subprocesses should pass the JWT to children and keep the PAT in the parent — if a subprocess is compromised, the JWT expires on its own.
Revocation, end-to-end
- Revoking the PAT stops future exchanges but doesn't invalidate JWTs already in flight. Those JWTs expire on their own clock; the API doesn't keep a revocation list for them. This is intentional — JWT lifetime is the revocation window.
- Reducing JWT lifetime by re-minting the PAT with a shorter exchange lifetime is the lever to pull if you need tighter post-revoke containment.
- Removing the underlying tenant grant (for example, revoking the user's access to the tenant) does immediately invalidate any in-flight JWT, since the scope is checked on every call.
Programmatic example: long-running marker poster
A service that posts invalidation markers throughout the day. Exchange once at startup, refresh proactively. Pseudo-code:
function getValidJwt() {
if (!jwt || isExpiringSoon(jwt)) {
jwt = exchangePatForJwt(process.env.AIRBRX_PAT);
}
return jwt;
}
async function postMarker(payload) {
const token = getValidJwt();
await fetch("https://api.airbrx.ai/markers", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(payload),
});
}
Where to go next
- Create a personal access token — the long-lived credential the exchange runs from.
- Scope a token — what the JWT will inherit.
- API reference — the full
/oauth/tokenshape.
Mint and exchange
Start with the PAT — exchange is a single curl call once you have one.
Create a personal access token