Menu

Authentication

Obtaining a key, the scope catalogue, and why an agent key returns 403s that look like bugs

Every /v1 request authenticates with an organization API key as a bearer token:

Authorization: Bearer kirora_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys begin kirora_ followed by 32 random bytes in base64url. There is no other credential — no signing, no client id, no OAuth flow.

The key identifies the organization, and every query is scoped to it. Only a partner key may act in another organization, one of its own tenants, by naming it in the Kirora-Workspace header; any other key sending that header gets 403, and so does a partner key sent from a browser (any request with an Origin header). A resource belonging to another organization returns 404, never 403, so that existence is not leaked.

Obtaining a key

Keys are created in the dashboard, not over the public API — key management is a cookie-session surface deliberately, so a leaked key cannot mint more keys.

The plaintext key is shown exactly once, at creation. Only a hash is stored, so a lost key cannot be recovered; issue a new one and revoke the old.

Scopes

A key carries an explicit list of scopes, and almost every operation requires exactly one (/v1/me, /v1/onboarding and /v1/webhooks/events require none; the Customer.io and Braze compatibility routes require both contacts:write and events:write). The reference shows which.

Resource Scopes
Campaigns campaigns:read, campaigns:write, campaigns:send
Contacts contacts:read, contacts:write, contacts:delete (erasure)
Events events:read (names and their properties), events:write (track, cancel)
Attributes attributes:read
Groups groups:read, groups:write
Segments segments:read, segments:write
Templates templates:read, templates:write (templates, assets, shared blocks)
Forms forms:read, forms:write
Journeys journeys:read, journeys:write, journeys:activate
Sending domains domains:read, domains:write
Suppressions suppressions:read, suppressions:write (import, unblock)
Topics topics:read, topics:write (a contact’s topic choice needs contacts:read / contacts:write)
Messaging transactional:send, messages:read (a sent message’s status and content), push:send, push:read (push projects)
Reports reports:read — the overview and per-campaign performance
Webhooks webhooks:write
Partner workspaces workspaces:manage (partner keys only)

Sending is a separate scope from writing throughout: campaigns:write lets a key build a campaign but not send it, and journeys:write lets it define a journey but not activate one. Grant the narrowest set that does the job.

Presets

For convenience a key can be created from a named preset instead of a scope list, and the preset is expanded at creation. Only the resulting scopes are stored, so a key issued today can never gain a scope added tomorrow — that is the reason presets are not stored as wildcards.

Preset Scopes
read_only Every :read scope
ingestion contacts:read, contacts:write, events:write, groups:read, groups:write
transactional transactional:send, messages:read, templates:read
full_access Every scope, including send and activate

Key kinds

This is the distinction most likely to look like a bug, so handle it explicitly.

Keys are one of three kinds. A partner key belongs to a platform that resells us and acts in the workspaces it manages for its own customers. An integration key is an ordinary server-to-server credential and behaves exactly as its scopes say.

An agent key is issued to an AI agent acting on the organization’s behalf. It reads, creates, edits and deletes campaigns, segments, groups, templates, journeys and domains like any other key — but it is gated on the send path:

  • It cannot send, schedule or activate anything. It may not hold campaigns:send, journeys:activate, transactional:send or push:send, so POST /v1/campaigns/{id}/send, /trigger, /schedule, /v1/journeys/{id}/activate, /v1/transactional/messages and /v1/push/messages all return 403 FORBIDDEN.
  • It can stop a send. Unscheduling or cancelling a campaign needs campaigns:write, and pausing or ending a journey needs journeys:write; stopping never sends anything. Resuming a paused journey is activation, so it stays with a person.
  • It cannot mutate contacts or consent: the contacts:write routes return 403 for an agent key even when it holds that scope, and it may not hold contacts:delete.
  • It cannot add contacts to a group, remove them from one, or track events: either can enrol contacts in a journey that then sends. POST /v1/groups/{id}/contacts and /contacts/remove return 403 for an agent key, and it may not hold events:write.
  • It cannot unblock or import suppressed addresses: it may not hold suppressions:write.
  • It may not hold webhooks:write: a webhook subscription streams contact data to any address the key names.
  • A campaign or journey an agent authored or edited is recorded and needs a human confirmation in the dashboard before it can be delivered. Until then, sending, scheduling or activating it — with any key — answers 409 AGENT_CONFIRMATION_REQUIRED.

So a 403 from an agent key on a send route is the system working, not a scope misconfiguration. Surface it as such: an SDK that reports “permission denied — check your scopes” sends people hunting for a problem that does not exist.

Note also that full_access cannot be used to create an agent key: the preset includes scopes agent keys may not hold (the four send scopes, contacts:delete, suppressions:write, events:write and webhooks:write), so creation fails validation rather than producing the 403 you were trying to test. Pass explicit scopes instead.

The boundary is enforced in the API, not in any client. It applies equally to curl.

Failure modes

Status Code Meaning
401 UNAUTHORIZED Header missing or malformed, or the key is unknown, revoked or expired
403 FORBIDDEN Key is valid but lacks the operation’s scope, or is an agent key on a gated route
403 API_KEY_IP_NOT_ALLOWED The request came from an address outside the key’s allowedIps
404 NOT_FOUND Resource does not exist or belongs to another organization

Restricting a key to your servers

A key can carry allowedIps: up to 50 addresses or CIDR ranges (203.0.113.7, 10.0.0.0/8, 2001:db8::/32). Requests from anywhere else are refused with 403 API_KEY_IP_NOT_ALLOWED. Set it when creating the key, or change it later from the dashboard; an empty list allows any address. Rotation keeps the list.

Handling keys

Treat a key as a password: environment or secret manager, never in source control, never in a browser or mobile app. A key grants access to the whole organization within its scopes. Rotate by issuing a new key, deploying it, then revoking the old one — both work during the overlap.

Next steps

  • ErrorsEvery code, including 401 UNAUTHORIZED and 403 FORBIDDEN
  • Rate limitsHow much each key may call
  • AgentsKeys for AI assistants and what they may not do
  • QuickstartMake your first requests with a key