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

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

Mint and exchange

Start with the PAT — exchange is a single curl call once you have one.

Create a personal access token