Scopes & Permissions

The fourteen mintable API key scopes, what each function actually checks, the admin super-scope, and the separate scope systems used by agent delegates and A2A negotiations.

Last reviewed September 18, 2026Fresh

Overview

Every API key carries an array of scopes that decides which endpoints it can reach. Scopes are chosen when the key is minted, in Dashboard > API keys > "Generate API key", and are stored on the key row as a text array. They cannot be edited afterwards; mint a new key instead.

Enforcement lives in one place. The shared authentication module compares the endpoint's required scope against the key's array, treating admin as satisfying everything, and refuses with a single shape:

HTTP/1.1 403 Forbidden
x-pb-reason: missing-scope

{ "error": "API key missing 'vc_issue' scope" }

When several scopes would do, the message joins them: API key missing 'metrics' or 'events' scope.

Dashboard session tokens are not scoped by this array. They are authorised by is_tenant_admin and are deemed to hold admin, so they pass every scope check. Scopes bound what a long-lived headless credential can do; a tenant admin acting in the console is the principal who mints those credentials in the first place.

Agent delegates have a separate scope system with its own vocabulary and its own narrowing engine. It has nothing to do with API key scopes and is covered further down this page.

Who can use this: every tenant on every plan. Some scoped endpoints are additionally plan-gated, and the enforcement table below names which.

Scope reference

These are the exact strings stored on the key row and offered by the key wizard. Fourteen can be minted; admin cannot.

ScopeLabelDescriptionExample endpoints
ingestSignal IngestSubmit signals, drive BLAST tunnels, call the predictive laddershield-ingest, shield-blast, shield-predict
eventsEventsRead processed events and metricsshield-metrics (prometheus action)
metricsMetricsRead tenant metrics without event accessshield-metrics (prometheus action)
vc_issueVC issueIssue credentials and create OID4VCI offersshield-vc-issue, shield-vc-oid4vci
vc_verifyVC verifyVerify presentations and build OpenID4VP requestsshield-vc-present
vc_revokeVC lifecycleRevoke, suspend, reinstate and rebuild the StatusList2021 bitstringshield-vc-status (writes)
spatialSpatial BindingAtomic Fingerprint capture and verificationshield-spatial-bind
scimSCIMSCIM v2 user and group provisioningshield-scim
okta_hooksOkta HooksOkta event hook consumershield-okta-hooks
dpopDPoPDPoP proof-of-possession bindingshield-dpop
supply_chainSupply chainSupply-chain attestation (Enterprise)shield-supply-chain
passkey_rpPasskey relying partyEnroll and verify passkeys for your domain (Enterprise)shield-passkey-rp
passkey_rp_importPasskey importBulk import existing passkey credentials (Enterprise)shield-passkey-rp
receiptsEntropy Provenance ReceiptsRead signed provenance receipts for artifacts the platform signed (Enterprise)shield-entropy-receipt
adminAdmin (super-scope)Satisfies every scope checkall scoped endpoints

Retired names. Until 2026-09-15 the wizard also offered playbooks, credentials, agents and shadow. No edge function has ever checked them, so a key carrying one has exactly the access any active key of that tenant has. Existing keys keep working; the names can no longer be minted, and the credential endpoints check vc_issue, vc_verify and vc_revoke by name.

Two scopes reach one endpoint between them. events and metrics gate the Prometheus exposition on shield-metrics and nothing else. Either one satisfies the check; the JSON actions on that function refuse a key whatever it carries. Event history is read from Dashboard > Events. Prometheus export has the scrape configuration.

Scope enforcement by function

What each function actually checks, read from its source. A plan column entry means the function applies that gate itself, after authentication.

FunctionAuth laneRequired scopePlan gate
shield-ingestkey, or admin session with x-pb-test-modeingest (or admin)none; Starter has a monthly volume cap
shield-blastkey or admin sessioningestEnterprise
shield-predictkey or admin sessioningest for predict, admin for ladder_statsEnterprise plus the ladder add-on
shield-vc-issuekey, or admin sessionvc_issuenone; needs credentials enabled
shield-vc-oid4vcikey or admin session for offervc_issue; metadata, token and credential need no keynone
shield-vc-presentkey or sessionvc_verify (or admin)none
shield-vc-statuskey or admin session for writesvc_revoke (or admin); reads are publicnone
shield-vc-verifykey, or admin session in test modenone checked; any active key of the tenantnone
shield-spatial-bindkey onlyspatial (or admin)Enterprise
shield-dpopkey for bind, verify, nonce; session for introspect and cleanupdpopnone
shield-supply-chainkey or sessionsupply_chainEnterprise
shield-entropy-receiptkeyreceiptsEnterprise
shield-passkey-rpkeypasskey_rp; passkey_rp_import for importEnterprise; sandbox keys refused
shield-scimkey as Authorization: Bearerscim literally; admin is not consulted herenone
shield-okta-hookskey as Authorization: Bearerany of okta_hooks, ingest, adminnone
shield-carrier-lookupkey or sessionnone checked; validates the key and the quotanone
shield-sdk-attestationkey or sessionnone checkednone
shield-cascadekey or sessionadmin, so in practice a sessionEnterprise
shield-metricssession; key for prometheus onlymetrics or events on the key lanenone
shield-provenance-guardsession (console lane)adminconsole-only
shield-breakglasssession (console lane)breakglassconsole-only

Functions with no scope option. shield-cross-reference, shield-shadow-proxy and shield-cached-proof take a dashboard session token only and are Enterprise-gated; shield-agent-delegate (except the public verify action) and shield-a2a-handshake take a session token only; shield-agent-trust takes the service-role key only.

Where no scope is checked, the module still validates that the key is active and belongs to the named tenant, and enforces the per-tenant quota where the function asks for it. Any active key of that tenant can call such an endpoint whatever its scopes.

Signal ingest has one carve-out. A key whose scopes array is empty is accepted if the key was created before 2026-09-03, and the response then carries x-pb-scope-warning: legacy-unscoped. A scope-free key created at or after that cutoff is refused like any other key that lacks the scope, so the carve-out cannot be used to mint a permanently unscoped credential.

The admin super-scope

admin satisfies every scope check. The rule lives in one helper, which every key-authenticated function calls rather than re-deriving:

export function keyHasScope(
  scopes: readonly string[],
  required: string | readonly string[],
): boolean {
  if (scopes.includes(ADMIN_SCOPE)) return true;
  const wanted = typeof required === "string" ? [required] : required;
  return wanted.some((s) => scopes.includes(s));
}

The matching refusal is equally centralised, which is why the 403 body and the x-pb-reason: missing-scope header are identical across the platform:

export function missingScopeResponse(required: string | readonly string[]): Response {
  const wanted = typeof required === "string" ? [required] : required;
  const label = wanted.map((s) => `'${s}'`).join(" or ");
  return errorResponse(`API key missing ${label} scope`, 403, {
    headers: { "x-pb-reason": "missing-scope" },
  });
}

`admin` cannot be minted. It is absent from the provisionable set in shield-admin-mutations and from the dashboard picker, so no admin can create a headless credential carrying it. Requesting it answers 400 Unknown scope(s): admin. breakglass is console-only for the same reason.

A dashboard session holds it. A tenant admin authenticated interactively is deemed to hold admin for the tenant they just passed is_tenant_admin on. That is the supervised path the minting barrier exists to funnel people into, and it is bounded to one tenant.

One endpoint ignores it. shield-scim checks for the literal string scim on the key and does not consult admin, so an admin-scoped key is refused there. That is the only exception on the platform.

The practical consequence: an endpoint whose required scope is admin, such as shield-cascade or the console-only provenance guard, is reachable from the dashboard and is not reachable with any key a tenant can create.

Least-privilege patterns

Mint one key per integration with only the scopes that integration calls. A key limited this way cannot be repurposed by whoever finds it.

Use caseLabelScopesWhy
Signal ingestion pipelineprod-ingestingestSubmitting signals is the whole job. Event history is read from the dashboard, so no read scope is needed.
Credential issuerprod-vc-issuervc_issue, vc_revokeIssue and manage lifecycle. Status reads need no key at all.
Credential verifierprod-vc-verifiervc_verifyVerification only; it cannot issue.
SCIM provisioningprod-scimscimDirectory sync only, and the literal scope is required here.
Okta event hooksprod-oktaokta_hooksHook consumption only.
Hosted passkeysprod-passkey-rppasskey_rpCeremonies only. Add passkey_rp_import to the migration key alone, and revoke it when the migration finishes.
Device attestation SDKprod-spatialspatialFingerprint enrolment and verification only.
Token bindingprod-dpopdpopBinding and verification only.

Anti-patterns.

  • One key with every scope. The wizard defaults to ingest and events for a reason. A key that can do everything turns any leak into a full compromise.
  • Sharing a key across services. You cannot revoke one consumer without breaking the others, so nobody revokes anything.
  • Reaching for `admin`. It cannot be minted, and wanting it usually means the endpoint is a console function with no public route.
  • Using a sandbox key in production. It is capped at 1,000 signals per rolling 30 days, its events are flagged as tests and excluded from your dashboards, and its outbound actions are simulated rather than sent.
  • Keeping an import scope around. passkey_rp_import exists for a migration. Revoke that key when the migration is done.

Rotating a scope set means minting a new key, moving traffic, and revoking the old one. Scopes on an existing key cannot be edited, and the browser has no write grant on the key table.

Agent scope narrowing

Agent delegates carry their own scope array, unrelated to API key scopes. The strings are yours: whatever you pass to create is stored, and the public verify action checks a requested scope against that array.

Each delegate row keeps the current scopes, the original_scopes recorded at delegation time, scope_narrowed_at, a narrowing_reason and the signal that caused it.

The trust engine narrows automatically. It scores recent behaviour and applies graduated reduction. The engine is a pure function; the database work happens in the calling function.

Trust scoreActionScopes retained
at or above 0.7nonefull original scopes, restored if they had been narrowed
0.4 to 0.699narrow_to_readthe read subset of the original scopes: ingest:read, events:read, playbook:read, intelligence:read, verify, plus verify if it was absent
0.2 to 0.399narrow_to_verifyverify only
below the delegate's floorrecommend_revocationempty array, and the delegate is deactivated

The revocation floor is per delegate. auto_revoke_below defaults to 0.2 and is set at creation time; it must be below initial_trust, otherwise the delegate would be revoked on its first assessment and the create call refuses it. A value outside the range falls back to 0.2. The floor is checked before the narrowing ladder, because an operator may set it above 0.2.

Signals that reduce trust.

SignalMaximum weightDetection
scope_violation0.35actions attempted outside the delegated scopes
velocity_spike0.30more than 50 actions in a 5-minute window
ai_behavioral0.30the intelligence worker's delta, scaled
error_burst0.25more than 10 failed actions in a 2-minute window
ip_rotation0.20more than 5 distinct IP digests in a 10-minute window
extended_inactivity0.15a burst after more than 24 hours of silence

Recovery is slow by design. With no negative signal detected and at least one activity row newer than the watermark, a normal_usage signal adds 0.05 per evaluation; an evaluation with nothing new to read leaves the score where it is. A delegate must demonstrate sustained stable behaviour to climb back. When the score passes 0.7 the engine compares the current scopes against the originals and restores them if they are smaller.

Hard signals bypass this entirely. A hard signal revokes every active delegate for the tenant inline during ingest and sets its trust score to zero. The trust engine is the graduated path for soft signals only.

A2A negotiation scopes

An agent-to-agent negotiation carries an allowed_scopes array of its own, derived rather than supplied.

The intersection is the ceiling. negotiate computes the intersection of the two delegates' scope arrays, so a negotiation can never grant more than the less privileged participant holds.

The trust state narrows it further. The combined trust coefficient is the geometric mean of the two trust scores, minus a network jitter penalty (up to 0.20 once jitter passes 150 ms), plus 0.05 when a spatial fingerprint is bound, plus up to 0.03 for a fresh SIM signal. That value maps to a state, and the state filters the intersection:

CoefficientStateScopes allowed
at or above 0.70fullthe whole intersection
0.40 to 0.699read_onlyintersection members containing read, plus verify
0.20 to 0.399verify_onlyverify only
below 0.20auto_revokednone; the negotiation is revoked

Transaction limits come from the same value, from a fixed tier table: 100,000 and 60 per minute at 0.90 and above, 25,000 and 30 at 0.70, 5,000 and 10 at 0.40, 500 and 3 at 0.20, and zero below that. They apply whatever the scopes say.

Re-attestation. status reports cache_valid. A negotiation must re-attest when its lifetime expires, when a hard signal arrives, or when the live geometric mean has drifted more than 0.10 from the cached coefficient. renew refuses a drifted negotiation with renewal_denied and action_required: "re-attest".

Revocation. A negotiation whose delegate is deactivated is revoked on its next renew. A hard signal revokes every active negotiation for the tenant inline during ingest, and the service-only invalidate action does the same on demand: status revoked, trust state auto_revoked, coefficient and limits zeroed and allowed_scopes emptied.

Session tokens and API keys compared

DimensionDashboard sessionAPI key
SourceSupabase session from signing inminted server-side by provision_api_key
HeaderAuthorization: Bearer <token>x-pb-api-key: <raw key>, or Authorization: Bearer <key> for SCIM and Okta hooks
Authorization checkis_tenant_admin(user, tenant)SHA-256 hash match on an active row for the tenant, then the scope check
Scope enforcementdeemed to hold admin, so every check passesthe key's own array, with admin satisfying everything
Tenant bindingthe tenant the admin check namedthe tenant that owns the key row
Rate limitingsame per-IP layers and per-tenant quotasame per-IP layers and per-tenant quota
Revocationsign out, or session expiryset is_active false through the revoke control
Audit actoractor_type: "user" with the user idactor_type: "api_key"

Test mode. x-pb-test-mode: true forces the session lane on endpoints that accept both, even when a key is present. Signal ingest uses it for the dashboard's test signal and flags the resulting event test_mode, which excludes it from metering and from the live filter in Dashboard > Events.

Legacy header. x-shield-api-key is accepted wherever x-pb-api-key is, and is checked second. New integrations should use x-pb-api-key.

Neither lane is a superset of the other. A key reaches endpoints the dashboard never calls, and several endpoints (cross-reference, shadow proxy, cached proofs, the delegate lifecycle, A2A, cascade) have no key lane at all.

Key management lifecycle

Creation. Dashboard > API keys > "Generate API key". The server generates the key as a prefix plus two UUIDs, SHA-256 hashes it, stores the hash with the first 16 characters as the display prefix, and returns the raw key exactly once. The browser holds no write grant on the key table, so this path is the only one.

The scopes are validated against the provisionable set: an unknown name answers 400 Unknown scope(s): <names>. The label is 1 to 120 characters, and at most 20 scopes may be requested.

Fields on the key row.

ColumnTypeDescription
iduuidprimary key
tenant_iduuidowning tenant
key_hashtextSHA-256 of the raw key
key_prefixtextfirst 16 characters, shown in the dashboard
labeltextyour label for the key, nullable
nametextlegacy column, defaults to Default Key
scopestext[]the scope array
is_sandboxbooleantrue for pb_test_ keys; set server-side so it always agrees with the prefix
is_activebooleanfalse once revoked
last_used_attimestamptzupdated after a successful authentication, without blocking the response
created_byuuidthe admin who minted it
created_attimestamptzcreation time

Rotation is mint, migrate, revoke. There is no in-place rotation and no automatic rotation for API keys. Watch last_used_at on the old key to confirm traffic has moved before revoking.

Revocation goes through the revoke_key action, which verifies your admin role and sets is_active false. It takes effect on the next request. A key id that is not this tenant's answers 404 API key not found for this tenant. There is no hard delete: the row stays for the audit trail, and the hash can never match again.

Audit. Provisioning writes api_key.provisioned with the display prefix, the scopes, the sandbox flag and the label. Revocation writes api_key.revoke with the key id. Neither records the key or its hash. Both appear in Dashboard > Audit log.

Related from the blog