1. Docs
  2. Authentication
  3. Custom Claims

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.

Each Environment keeps its own list, so Development and Production can map different claims. It holds 20 claims at most.Metadata and an external id are written only by your backend, with a secret API key, or by a Console or Workspace administrator. An organization's own administrators can rename their organization but never set its metadata or external_id (organization.developer_field), so a claim read from either is one you can make decisions on. A name field can come from sign-up, single sign-on, social sign-in or directory sync.A change to the list, or to the metadata it reads, reaches the next token issued. Tokens already issued keep what they carry until they expire.Tokens for machine clients carry no custom claims: a machine client has no metadata to read.

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_idThe identity's own fields.
organization.name, organization.slug, organization.external_idThe 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

Names. A claim's name holds letters, digits, _, -, ., : and /, up to 64 characters, so a namespaced name such as https://example.com/plan fits. A name the JWT or OpenID Connect standards define, or one Canopy sets on a token (sub, email, type, the _id and _slug claims, permissions, anything starting org_ or mcp_), is refused with token_claims.reserved_name, so a custom claim never stands in for one of Canopy's.Tokens. Each entry goes on the access token, the ID token or both. Only Hosted Login issues an ID token; the Direct API's tokens carry the entries marked for the access token. One name twice on the same token is refused with token_claims.duplicate_name; the same name on different tokens is allowed.Missing values. A value that is not stored, or is null, leaves its claim out of the token. An organization source with no organization in the session leaves it out too.Types. A value keeps its JSON type: a string, number, boolean, object or array arrives as it was stored.Size. The custom claims on one token stay within 2,048 bytes of JSON. They are added in the list's order; a claim that would pass the limit is left out, and the token then carries custom_claims_overflow: true. A sign-in never fails because of a custom claim.

Map claims in the Console

You need auth_settings.manage; with auth_settings.view the list shows read-only.

Open Authentication and the Custom claims tab. Each Environment is a row, with the claims it maps under Claims.Expand the Environment's row and click Add claim.Enter the Claim name, choose where the value comes from under Value from (Identity metadata, Organization metadata or a profile field), and for metadata enter the Metadata path, such as plan, with dots for nested keys.Tick Access token, ID token or both, and click Save changes.

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",
  "...": "..."
}
Environment
API version
v1.0
On this page Was this page helpful?

Tell us how we can improve this guide.