Skip to content

API keys — scopes ​

Exported from api-key-scopes.ts. Covers ApiKeyScopeSchema — the fixed set of permissions an API key can carry, independent of tenantId (whose data) and environment (live/test).

ApiKeyScopeSchema ​

ts
import { ApiKeyScopeSchema, API_KEY_SCOPES, type ApiKeyScope } from '@klappay/types'

ApiKeyScopeSchema.parse('charges:write') // 'charges:write'
API_KEY_SCOPES // readonly array of every valid scope string

A flat, closed set of 14 strings — no wildcard, no named roles. A key missing a scope a route requires gets 403 insufficient_scope on that route; a key with none of these scopes can still authenticate (a bad key is 401, a key lacking permission is 403 — two different failure classes to branch on).

ScopeGrants
charges:readGET /v1/charges, GET /v1/charges/{id}, /timeline, /events, /qrcode
charges:writePOST /v1/charges
charges:split_writeRequired in addition to charges:write whenever a POST /v1/charges request includes splitRecipients — a key without it can create ordinary charges but never redirect any part of the payout, even to an already-registered recipient
webhooks:readGET /v1/webhooks, GET /v1/webhooks/{id}/deliveries, GET /v1/webhooks/listen
webhooks:writePOST /v1/webhooks, DELETE /v1/webhooks/{id}, POST /v1/webhooks/{id}/deliveries/{id}/retry
webhooks:manage_secretPOST /v1/webhooks/{id}/rotate-secret — kept separate from webhooks:write since it returns a new plaintext secret, the single highest-trust action on this resource
metrics:readPOST /v1/metrics/query, every resource (charges, transactions, distributions)
metrics:charges:readPOST /v1/metrics/query with resource: 'charges' only
metrics:transactions:readPOST /v1/metrics/query with resource: 'transactions' only
metrics:distributions:readPOST /v1/metrics/query with resource: 'distributions' only
sandbox:triggerPOST /v1/sandbox/charges/{id}/trigger — kept independent of charges:write even though both mutate charge state; simulating a state transition and creating a real charge are different capabilities
recipients:readGET /v1/recipients, GET /v1/recipients/{id}/transferability
recipients:writePOST /v1/recipients, DELETE /v1/recipients/{id} — registers or revokes a recipient (an address eligible to be referenced by recipientId in a split); does not by itself make it eligible to become a key's payoutAddress, and revoking a recipient this scope registered has no effect on any existing key — that only happens for a recipient that separately went through recipients:manage_payout (see that scope). Deliberately weak in this respect: recipients:write is safe to hand to a lower-trust or third-party integration key precisely because it can never be used to disable another key
recipients:manage_payoutPATCH /v1/recipients/{id} — the only way a recipient becomes eligible to be used as an API key's own payoutAddress. Deliberately separate and stricter than recipients:write; meant to be held only by a key that already went through its own out-of-band approval for this specific action (e.g. Dashboard's internal key, after session + owner role + email confirmation on its side) — never a merchant-facing or third-party integration key. Revoking a recipient that currently has payout: true has a second effect beyond blocking future splits: any existing API key whose own payoutAddress matches it stops authenticating entirely (401 payout_address_revoked) on its very next request. This is deliberately gated on payout: true, not any revoked recipient — payoutAddress is public (the first entry in the on-chain split), so gating it on the weaker recipients:write scope would let any integration key permanently deny any other key by registering-then-revoking its address

metrics:read and the three metrics:{resource}:read scopes aren't mutually exclusive alternatives — a key can hold either, or both. A key with only metrics:charges:read can query charges metrics but gets 403 insufficient_scope querying transactions/distributions; a key with metrics:read can query all three regardless of which narrower scopes it also has.

charges:split_write and recipients:write/recipients:manage_payout are mutually exclusive — no key can ever hold charges:split_write together with either recipients-registration scope (CONFLICTING_SCOPE_PAIRS, findConflictingScopes, exported from this same package). Core's authMiddleware rejects such a key outright (403 conflicting_api_key_scopes) before any route runs, regardless of whether the minting UI also refuses to issue one — the ability to register a recipient and the ability to route a charge's split to one must live on separate keys, or a single leak defeats the whole point of requiring recipients to be pre-registered. A key-minting UI should call findConflictingScopes(requestedScopes) itself and refuse the combination before ever calling Core, rather than relying on this check as the only line of defense.

Two authenticated routes aren't gated by any scope at all — GET /v1/networks (the capabilities matrix, identical for every key in an environment, nothing tenant-specific to protect) and GET /v1/distributions/pending//pending/events (a cross-tenant public feed by design — distribute() on 0xSplits is permissionless on-chain, so this endpoint exists for discoverability, not to grant a capability that isn't already open to anyone). Both still require a valid, unrevoked, environment-matched key — they just don't check scopes.

Public docs for @klappay/types, mirrored from this package's own docs/ folder.