SenseCrypt Docs
Guides

Customize claims and scopes

Control which profile claims SenseCrypt releases into ID tokens, access tokens, UserInfo, and SAML assertions — using typed attributes, scopes, and per-app scope bindings.

SenseCrypt decides which profile data ends up in your tokens through two building blocks: typed attributes (the fields your directory can hold) and scopes (the switches that release those fields as claims). This guide shows how to shape the claims your application receives.

The model in one minute

  • An attribute is a typed field on a user — email, name, a custom department, and so on. Attributes are validated when written.
  • A scope is a named bundle of attributes. When a scope is granted (OIDC) or attached to an SP (SAML), the attributes it bundles are released as claims — but only if the user actually has a value.
  • Each application has a set of bound scopes. A scope your app can't request can't release its claims.

The rule that ties it together:

A claim is released only when a granted/attached scope releases it and the user has a value for it. No value → the claim is simply omitted (there is no backfill).

Standard scopes

Four scopes are seeded in every tenant and behave as in OIDC:

  • profile — a curated bundle of profile attributes. This is a best-effort bundle you control: you choose which profile attributes it releases.
  • email, phone, address — these have fixed membership (you can't add or remove attributes from them), matching the OIDC spec.

Two protocol scopes are always available and bundle no attributes: openid (marks an OIDC request) and offline_access (requests a refresh token).

A fresh tenant's profile scope releases only name by default. The extended profile attributes (given/family name, birthdate, gender, locale, and so on) exist in the catalog but start unattached — add them to profile if you want them released.

Define a custom attribute

Custom attributes let you carry your own fields (a department, an employee_id, a plan_tier). Each attribute is typed, and the type is enforced at write time. Supported data types include:

TypeNotes
stringLength-constrained.
integer / decimalRange- and precision-constrained.
booleanAccepts common truthy/falsy spellings.
dateYYYY-MM-DD, real-date checked.
choiceSingle-select from a fixed option list.
phone_numberParsed and stored canonical E.164.
timezone / localeValidated against IANA / BCP-47 lists.
couponA choice-like type whose option list is a secret (see below).

To add one, create the attribute definition in the admin console (or via the Management API), giving it a snake_case key, a data type, and any constraints.

Write authority (provenance)

Every attribute carries a provenance flag that controls who may write it — independent of who may read it:

  • self_supplied — the user can set it during mobile signup or profile edit.
  • administered — only the admin console or your SCIM connector may write it. The phone never even asks for administered attributes.

Use administered for fields your business controls (entitlements, tiers) and self_supplied for fields the user owns.

Release an attribute through a scope

Defining an attribute does nothing on its own — it has to belong to a scope to be released. You have two options:

  1. Add it to profile (or another standard release scope) if it should ride along with the standard profile claims.
  2. Create a custom scope (for example department) and add the attribute to it, so your app can request that data explicitly.

An attribute is "active" precisely when it belongs to at least one scope — membership is the single on/off switch. Within a scope you can also mark an attribute required (enforced at write time, on signup and admin add-user) and set its display order.

Bind scopes to your application

Finally, the application must be allowed to request the scope. In the app's configuration, add the scope to its bound scopes. An app with no explicit bindings defaults to profile and email; phone, address, and any custom scopes are opt-in per app.

Now, when your app requests that scope at /authorize:

GET /v1/idp/oidc/authorize?client_id=...&scope=openid%20profile%20department&...

the released attributes appear as claims in the ID token and at UserInfo.

Where release is enforced

The release gate is applied consistently at every layer, so there's no way to smuggle a claim through a side door:

SurfaceWhat happens
PAR / /authorizeRequested scopes must be a subset of the app's bound scopes (plus protocol scopes), else invalid_scope.
ID tokenClaims are emitted only for attributes whose scope membership intersects the granted scopes.
UserInfoSame gate as the ID token.
SAML assertionThe SP's attached scopes are the release gate.
Discoveryscopes_supported / claims_supported advertise only what a live release scope releases.

SAML: the attribute map only renames

For SAML service providers, release works the same way — the SP's attached scopes decide what's released. The SP's attribute map then renames released claims onto SAML attribute names (and sets the NameFormat). Crucially:

The SAML attribute map can only rename, never release. A mapping entry for a claim whose releasing scope isn't attached to the SP is inert — the claim is simply absent from the assertion.

So to get a claim into a SAML assertion, first attach the scope that releases it, then map it to the name your SP expects (for example the URI NameFormat Entra ID wants).

The coupon special case

A coupon attribute is a choice-like type where the option list is a secret — codes a signing-up user must type correctly. Two things to know:

  • Options never reach the device. A coupon field goes to the phone as a plain text input; the valid codes stay server-side.
  • Validation is server-side and case-insensitive, storing the canonical casing. Because the phone can't check the code locally, a dry-run validation endpoint lets the app surface a bad code before the face-capture step.

Reserved claims are protected

You cannot define a custom attribute that overwrites a protocol claim. Keys like sub, iss, aud, exp, iat, jti, nonce, azp, scope, sid, and permissions are reserved — a custom attribute with one of these keys can never overwrite the server-minted claim in an issued token.

Using the Management API

Everything above can also be driven from the Management API with curl, so an automation engineer can provision attributes and scopes as code. These calls are the programmatic equivalent of the console screens — the release rules are identical because the gate lives in the server, not the UI.

These calls need an M2M access token. Mint one with the client-credentials grant and pass it as Authorization: Bearer {mgmtToken}. The token acts only within the tenant it was minted for, and only up to the capabilities its bound roles carry — so the role behind {mgmtToken} must grant the capability each call needs: attributes:create / attributes:read / attributes:update for attribute definitions, scopes:create / scopes:read / scopes:update for scopes and their membership, and oidc_apps:update to bind a scope to an app. A call whose capability the role doesn't hold returns 403.

The base path is the tenant admin API — https://{tenant-host}/v1/admin — where {tenant-host} is your tenant's admin host. (The admin host can differ from the issuer host you mint the token on; confirm it with your operator.) Send request bodies as JSON with Content-Type: application/json.

1. Define a custom attribute

POST /v1/admin/attribute_definitions creates a typed attribute (the console "add attribute" form). Give it a snake_case key, a data_type, a label, and a provenance (administered or self_supplied). A choice or coupon type also takes an options list, and any type may carry constraints. The optional scopes array attaches the attribute to release scopes right away — omit it and the attribute defaults into profile.

curl -X POST "https://{tenant-host}/v1/admin/attribute_definitions" \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "team",
    "data_type": "string",
    "label": "Team",
    "provenance": "administered",
    "constraints": { "max_length": 64 },
    "scopes": [{ "scope": "profile", "required": false }]
  }'

The 201 response is the stored attribute, including the id you reference later and the scopes it now belongs to:

{
  "id": "3f1c8b1e-0c2a-4a9b-9e2d-1a2b3c4d5e6f",
  "key": "team",
  "kind": "custom",
  "data_type": "string",
  "label": "Team",
  "provenance": "administered",
  "scopes": [{ "scope": "profile", "required": false, "ordering": 2 }],
  "options": [],
  "constraints": { "max_length": 64 },
  "created_at": "2026-07-16T10:00:00Z",
  "updated_at": "2026-07-16T10:00:00Z"
}

2. Create a custom scope

If you'd rather release the attribute through its own scope, POST /v1/admin/scopes creates a custom scope. The body takes a snake_case name and an optional description. (Standard scopes are seeded, not created.)

curl -X POST "https://{tenant-host}/v1/admin/scopes" \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "team",
    "description": "Releases the team claim"
  }'

The 201 response is the new scope; members_editable and released are both true for a custom scope:

{
  "id": "9a2b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
  "name": "team",
  "description": "Releases the team claim",
  "kind": "custom",
  "members_editable": true,
  "released": true,
  "attributes": [],
  "created_at": "2026-07-16T10:01:00Z",
  "updated_at": "2026-07-16T10:01:00Z"
}

3. Attach the attribute to a scope

There are two paths, matching the two options above:

  • Add it to profile (or another release scope) by patching the attribute's memberships: PATCH /v1/admin/attribute_definitions/{def_id} with a scopes array. The array replaces the attribute's release-scope memberships (locked standard memberships are preserved), so include every release scope you want it in.
  • Add it to a custom scope's membership list with POST /v1/admin/scopes/{scope_id}/attributes. The body is an items array of { attribute_id, required, ordering } — this is the rich per-scope editor, so it also sets the per-scope required flag and display ordering.

Both {def_id} and {scope_id} are UUIDs. If you don't already have them, list with GET /v1/admin/attribute_definitions?q=team and GET /v1/admin/scopes?q=team — each returns { items, total, limit, offset }.

curl -X POST "https://{tenant-host}/v1/admin/scopes/{scope_id}/attributes" \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "attribute_id": "3f1c8b1e-0c2a-4a9b-9e2d-1a2b3c4d5e6f", "required": false, "ordering": 0 }
    ]
  }'

The 201 response reports how many memberships were attached (or upserted):

{ "attached": 1 }

The spec-fixed scopes email, phone, and address reject membership edits — their contents are fixed by OIDC (members_editable is false). profile and custom scopes are editable.

4. Bind the scope to your application

Scope binding lives on the OIDC application resource, not the scopes API. Replace an app's bound scope set with PATCH /v1/admin/oidc-apps/{client_id}, sending a scopes array of scope names (this needs the oidc_apps:update capability):

curl -X PATCH "https://{tenant-host}/v1/admin/oidc-apps/{client_id}" \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{ "scopes": ["profile", "email", "team"] }'

Now the app may request team at /authorize, and the attribute is released as a claim under the same rules described above.

For SAML service providers there is no scopes-API equivalent: attaching release scopes to an SP and editing its attribute map are managed on the SAML app resource — see SAML SSO.

On this page