# Custom claims

Attach facts about a user, such as a plan or a tenant, that reach your backend at sign-in and in every agent token.

A custom claim is a fact about one user that your backend wants at sign-in without a second lookup: their plan, a tenant id, a support tier, a flag. You set claims per user, from the console or with an API token. The person can read their own and cannot change them.

```bash
curl -X PUT https://api.console.buildbase.app/api/users/6650f1c2a1b2c3d4e5f60718/claims \
  -H "Authorization: Bearer $BUILDBASE_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"claims": {"plan": "pro", "tenant": "acme", "limits": {"seats": 25}}}'
```

The token is an org API token whose role can update users, made under **Settings → API keys** (see [Admin API](/reference/admin-api)). The route answers `{ claims }`. In the console, the same object is the **Custom claims** card on the user's page, as JSON.

## The rules

| Rule   | Limit                                                                                                                 |
| ------ | --------------------------------------------------------------------------------------------------------------------- |
| Shape  | A JSON object. `{}` clears every claim                                                                                |
| Names  | A letter, then letters, digits or underscores, 64 characters at most. Keys inside nested objects follow the same rule |
| Depth  | Values nest at most 3 levels deep                                                                                     |
| Size   | 2,048 bytes as JSON                                                                                                   |
| Values | Strings, finite numbers, `true`, `false`, `null`, arrays and objects                                                  |

A save replaces the whole object, so send every claim you want to keep. One that breaks a rule is refused with `400 invalid_claims` and a sentence naming the problem, and nothing is saved.

Each save writes `user.claims_updated` to the security audit log, with the claim names and never their values: a claim can hold anything.

## Where they arrive

| Where                           | Field                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------ |
| The code exchange               | `data.user.claims` from `POST /api/v1/auth/token`                              |
| Your backend, holding a session | `claims` from `GET /api/v1/public/profile`                                     |
| The browser                     | `useSaaSAuth().user.claims`                                                    |
| Your Application Token URL      | `claims` in the identity token BuildBase signs, `AppTokenRequestClaims.claims` |

They are `{}` when the user has none, so reading a key needs no guard on the object itself. They always sit under `claims` and never beside `sub`, `email` or `role`, so a claim called `role` cannot stand in for the real one.

At the code exchange, in the route that [sets the session cookie](/quick-start/production):

```typescript
const { data } = await res.json(); // { sessionId, user }
const plan = data.user.claims.plan ?? 'free';
```

For OAuth2 clients and agents, BuildBase sends them to your Application Token URL on every grant and every refresh. Copy what the token needs into it:

```typescript
import { handleAppTokenRequest, mintAgentToken } from '@buildbase/sdk';

const { status, body } = await handleAppTokenRequest({
  authorization: request.headers.get('authorization'),
  clientSecret: process.env.BUILDBASE_AGENT_CLIENT_SECRET!,
  mintToken: (claims) =>
    mintAgentToken({
      claims,
      secret: process.env.SYSTEM_SECRET!,
      extraClaims: { plan: claims.claims?.plan },
    }),
});
```

## When a change shows

The next code exchange, profile read and token mint carry it. The SDK reads claims with the profile when the session loads, so a tab that is already open sees the change after a reload. A deleted account loses its claims with the rest of its data.

## Next steps

- [Signing in as a user](/authentication/sign-in-as) - see your app as the person, claims and all.
- [MCP and agents](/reference/mcp-agents) - the Application Token URL that receives them.
- [Admin API](/reference/admin-api) - tokens, roles and permissions.
