> ## Documentation Index
> Fetch the complete documentation index at: https://glide-9da73dea.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Exchange client credentials for a one-hour ES256 bearer token at /api/partner/oauth/token, then send it on every partner request.

Glide issues native `client_credentials` JWTs. There is no authorization server to configure, no dynamic registration, and no static bearer token.

## Get your credentials

A Glide admin creates your partner record and returns `client_id` and `client_secret` **once**. Client IDs look like `gpc_<slug>_<8 hex>` and secrets like `gps_` followed by 32 random bytes in base64url. Both contain only URL-safe characters.

Store the secret in your server secret manager. Glide stores only a salted scrypt hash and can never show it to you again. If you lose it, ask Glide to rotate the secret.

## Mint a token

`POST https://glide.co/api/partner/oauth/token`

Send `Content-Type: application/x-www-form-urlencoded` with a required `grant_type=client_credentials` and an optional space-separated `scope`. Authenticate with HTTP Basic: form-encode each credential, then base64-encode `client_id:client_secret` per RFC 6749 section 2.3.1.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST https://glide.co/api/partner/oauth/token \
    --user "$GLIDE_CLIENT_ID:$GLIDE_CLIENT_SECRET" \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode 'scope=kyc:read wallet:read'
  ```

  ```ts TypeScript theme={null}
  const basic = Buffer.from(
    `${encodeURIComponent(process.env.GLIDE_CLIENT_ID!)}:${encodeURIComponent(
      process.env.GLIDE_CLIENT_SECRET!
    )}`
  ).toString('base64');

  const res = await fetch('https://glide.co/api/partner/oauth/token', {
    method: 'POST',
    headers: {
      Authorization: `Basic ${basic}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      scope: 'kyc:read wallet:read',
    }),
  });

  const token = await res.json();
  console.log(token.access_token, token.expires_in, token.scope);
  ```
</CodeGroup>

You may instead send `client_id` and `client_secret` as form fields. Do not do both. Combining the two authentication methods, or repeating any parameter, returns `invalid_request`. Form bodies are limited to 4096 bytes.

Response:

```json theme={null}
{
  "access_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "kyc:read wallet:read"
}
```

Every token response carries `Cache-Control: no-store` and `Pragma: no-cache`.

## Use the token

Send `Authorization: Bearer <access_token>` on every resource request.

```bash theme={null}
curl --request GET \
  "https://glide.co/api/partner/users/11111111-1111-4111-8111-111111111111/kyc" \
  --header "Authorization: Bearer $GLIDE_ACCESS_TOKEN"
```

## Cache the token

Tokens live exactly one hour. Minting one per request will hit the 30 per minute issuance limit and waste latency on every call. Cache the token in process and refresh it shortly before expiry.

```ts theme={null}
type Cached = { token: string; expiresAt: number };
let cached: Cached | null = null;

export async function getAccessToken(): Promise<string> {
  const now = Date.now();
  if (cached && now < cached.expiresAt) return cached.token;

  const basic = Buffer.from(
    `${encodeURIComponent(process.env.GLIDE_CLIENT_ID!)}:${encodeURIComponent(
      process.env.GLIDE_CLIENT_SECRET!
    )}`
  ).toString('base64');

  const res = await fetch('https://glide.co/api/partner/oauth/token', {
    method: 'POST',
    headers: {
      Authorization: `Basic ${basic}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({ grant_type: 'client_credentials' }),
  });

  if (!res.ok) throw new Error(`glide token ${res.status}: ${await res.text()}`);

  const body = (await res.json()) as { access_token: string; expires_in: number };
  // Refresh 60 seconds early so an in-flight request never carries a dead token.
  cached = {
    token: body.access_token,
    expiresAt: Date.now() + (body.expires_in - 60) * 1000,
  };
  return cached.token;
}
```

Treat a single `401 invalid_token` as a signal to drop the cache and mint once more. Do not loop. If the second attempt also fails, the credentials or the client status is the problem, not the cache.

Rotating your secret changes your cache key. Clear the cached token when you deploy a new secret.

## Token claims

Partner tokens share the agent issuer's ES256 signing key and issuer, published at `/.well-known/agent-jwks.json`. You do not need to verify them yourself, but the claims are stable if you want to inspect them.

| Claim        | Value                                 |
| ------------ | ------------------------------------- |
| `iss`        | `https://glide.co` by default         |
| `aud`        | `partner.glide.co`                    |
| `typ` header | `at+jwt`                              |
| `sub`        | your `client_id`                      |
| `client_id`  | your `client_id`, matching `sub`      |
| `scope`      | space-separated granted scopes        |
| `jti`        | UUID                                  |
| `iat`, `nbf` | issue time, with `nbf` equal to `iat` |
| `exp`        | `iat` plus 3600                       |

Glide verifies locally with JOSE against its configured public JWK. Verification permits only ES256, checks issuer, audience and type, enforces the one-hour maximum lifetime, and allows 30 seconds of clock skew. Missing or malformed key configuration returns `503 auth_unavailable` on the resource API. Invalid tokens return `401 invalid_token`.

## Scopes

Two different things are called scopes, and both must line up.

**Token entitlements** are what your client may request. They are recorded on your partner record. They default to the five consent scopes, and an admin can additionally authorize `treasury:write`.

**User consent scopes** are what an individual user approved for you. See [Users and consent](/partners/users-and-consent).

| Scope             | Kind       | Grants                                                         |
| ----------------- | ---------- | -------------------------------------------------------------- |
| `kyc:read`        | consent    | Verification status, and requesting a hosted verification link |
| `wallet:read`     | consent    | EVM and Solana public addresses                                |
| `us_account:read` | consent    | Provisioned personal USD receiving details                     |
| `transfers:read`  | consent    | Personal ledger history and settlement or failure events       |
| `payouts:receive` | consent    | Permission to receive your payouts                             |
| `treasury:write`  | token only | Push batch payouts from your treasury                          |

Omitting `scope` on the token request asks for all of your current entitlements. An explicit `scope` must be a subset of them, otherwise you get `invalid_scope`. Empty entitlements produce an empty scope string.

<Warning>
  `treasury:write` is a token permission only. It never appears in a user grant or in consent copy, and it never substitutes for the user's `payouts:receive` consent. Holding a token entitlement grants you nothing about any user.
</Warning>

Admin entitlement changes apply to newly issued tokens. Claims inside an already-issued JWT stay as they were until it expires.

## Rotation and suspension

Ask Glide to rotate your client secret when you suspect exposure or on a schedule you set. Rotation replaces the stored hash immediately and keeps your `client_id` unchanged. The old secret can no longer mint tokens, but JWTs already issued stay valid until they expire, subject to the verifier's clock tolerance.

To cut off access right now, ask Glide to suspend the partner. Suspension disables resource access and webhook delivery immediately.

Token issuance holds a lock on your partner row through verification and signing, so a concurrent rotation, entitlement change or suspension is ordered against issuance rather than racing it.

## Rate limits and errors

Token issuance allows 30 requests per minute per registered client. Wrong-secret and suspended-client attempts count against the same budget. Unknown clients perform the same scrypt derivation against a dummy hash, so timing does not reveal whether a client exists.

Token errors use the OAuth JSON envelope, which is different from the resource API envelope:

```json theme={null}
{ "error": "invalid_client" }
```

| HTTP | Error                     | Meaning                                                                                                                  |
| ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 400  | `invalid_request`         | Malformed body, both auth methods used, or a repeated parameter                                                          |
| 400  | `unsupported_grant_type`  | `grant_type` was not `client_credentials`                                                                                |
| 400  | `invalid_scope`           | Requested scope is not a subset of your entitlements                                                                     |
| 401  | `invalid_client`          | Unknown client or wrong secret, with `WWW-Authenticate: Basic realm="glide-partner"`. Both cases return identical bodies |
| 403  | `invalid_client`          | Valid credentials on a suspended or pending client                                                                       |
| 429  | `rate_limited`            | Over 30 per minute, with `Retry-After: 60`                                                                               |
| 503  | `temporarily_unavailable` | Signing or audit persistence is unavailable. Retry                                                                       |

A 503 here means Glide withheld the token rather than issue one it could not audit. Retry with backoff.
