Custom Claims
Put facts of your own, such as a plan or a seat count, on the tokens Canopy issues, copied from identity or organization metadata, so your backend reads them without another request.
How custom claims work
Each Environment keeps a list of custom claims. An entry names the claim, the stored value it copies, and the tokens it goes on: the access token, the ID token, or both. Every time Canopy issues a person's token, at sign-in, on a refresh and on a switch of organization, it reads each value afresh and adds the claim, so plan: "pro" reaches your backend inside the token it already verifies.
Where a value comes from
A claim's source is one of these, written out in full:
| Source | What it reads |
|---|---|
identity.metadata.<path> | A key in the identity's metadata, with dots for nested keys (identity.metadata.billing.plan), up to 5 levels deep. Set through PATCH /api/v1/identities/{id}. |
organization.metadata.<path> | The same in the metadata of the organization the session is acting in. Set through PATCH /api/v1/organizations/{id}. |
identity.email, identity.email_verified, identity.first_name, identity.last_name, identity.external_id | The identity's own fields. |
organization.name, organization.slug, organization.external_id | The fields of the organization the session is acting in. |
A metadata key holds letters, digits, _ and -. Any other source, such as a field Canopy keeps to itself, is refused with token_claims.source_invalid.
What a token carries
Map claims in the Console
You need auth_settings.manage; with auth_settings.view the list shows read-only.
plan, with dots for nested keys.The editor explains a name, path or token choice the API would refuse before you save. If another administrator changed the Environment's settings while you were editing, the save is refused, your edit is dropped, and the tab shows what the Environment holds now.
Map claims over the API
GET /api/v1/token-claims reads the list of the Environment your API key belongs to, with token_claims.view. PUT /api/v1/token-claims replaces the whole list, with token_claims.manage; an empty list removes every custom claim. Both answer the list and the Environment's version: send it back as If-Match and a save over a newer change answers 409 version_mismatch instead of replacing it.
curl -X PUT https://auth.canopy-io.com/api/v1/token-claims \ -H "X-API-Key: $CANOPY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "claims": [ { "claim": "plan", "source": "organization.metadata.billing.plan", "tokens": ["access", "id"] }, { "claim": "seats", "source": "organization.metadata.billing.seats", "tokens": ["id"] } ] }'
{ "data": { "version": 4, "claims": [ { "claim": "plan", "source": "organization.metadata.billing.plan", "tokens": ["access", "id"] }, { "claim": "seats", "source": "organization.metadata.billing.seats", "tokens": ["id"] } ] } }
A refused entry answers 400 with its code and error.data naming the entry's index and the field at fault. Every save is recorded in the audit log as environment.token_claims_updated, with the list before and after. The list also travels with an Environment's export and import.
Read a claim
Verify the token as you already do, then read the claim from its payload like any other. A claim can be absent, when its value is not stored, so treat a missing claim as "not set" rather than as an error. When a token carries custom_claims_overflow, some claims did not fit: shorten the values, or map fewer claims to that token.
{ "sub": "7d3f9b2e-4c1a-4e8b-9f2d-1a6c5e0b8f34", "type": "identity", "org_id": "0b6a2f1c-5d3e-4a7b-8c9d-2e1f0a3b4c5d", "plan": "pro", "...": "..." }
Tell us how we can improve this guide.