Authentication and keys
Bearer authentication, exact capability and Operation access, expiration, rolling budgets, and key delegation.
The header
Every /api/v1 request authenticates with the same header:
Authorization: Bearer spore_sk_...The bare key without the Bearer prefix is also accepted for clients that cannot set a scheme. Anything else is a 401. A key belongs to an organization. Everything it reads and spends is that organization's.
Capability scopes
Every key carries an explicit capability set. The console offers Observer (read-only), Researcher, and Operator presets, or Custom access for individual scopes. Presets do not include key management; select it explicitly only for a trusted administrator.
| Scope | Authority |
|---|---|
company:read | Read Companies and held Company data. |
company:write | Create, correct, and delete Companies. |
company:unlock | Unlock Legal entity data for Companies (priced). |
search:run | Run Commons searches (priced). |
segment:read | Read Segments and their members. |
segment:write | Create, update, and launch Segments (includes priced work). |
enrich:read | Read enrichment Jobs and results. |
enrich:run | Start Company enrichment (priced). |
knowledge:write | Record organization knowledge. |
export:run | Export held data. Export is free; Unlock remains the data gate. |
wallet:read | Read credit balances and transactions. |
events:read | Read activity events. |
key:manage | Create and revoke organization API keys, subject to the delegation rules below. |
A key missing the required scope receives a structured 403:
{
"error": "scope_required",
"message": "This API key does not carry the required capability scope.",
"requiredScope": "search:run",
"scopes": ["company:read", "segment:read"]
}Exact Operation allowlist
Scopes and allowedOperations are independent gates: every request must pass both. Use exact Operation IDs from the Operation contract (the IDs shown in the console), not REST paths or wildcard patterns.
- Omitted or
null: all Operations permitted by the key's scopes, including future Operations under those scopes. []: deny every Operation.- An array such as
["search"]: only those exact Operations, and only with their required scopes.
An allowlist never grants a missing scope. The Door refuses a disallowed Operation before input validation or idempotency replay; it cannot recover an earlier response through a now-disallowed Operation.
Expiration and console lifecycle
Organization owners and admins can mint keys in Organization → API keys → Create API key. Name the client, choose a preset or Custom access, and set the rolling budget and per-request cap for priced scopes. Open Show exact access to choose individual custom scopes, Allowed Operations, and Expires in (30, 60, 90, 180, or 360 days). Choose Only selected Operations with no checkboxes to deny all. Custom non-priced management keys have no spending bounds.
Copy the secret from the success step: it is shown once and cannot be recovered. Open the key's detail page to inspect scopes, allowlist, expiry, limits, rolling burn, and history. Owners/admins can Revoke API key there and confirm the Decision. Expired or revoked credentials cannot authenticate. Mint a replacement rather than expecting revocation to be reversible.
Mint and revoke with Operations
Key management is two free Operations: key-create (POST /api/v1/keys) and key-revoke (POST /api/v1/keys/{keyId}/revoke). Both require key:manage, the relevant Operation in any exact allowlist, and a creator who is still an owner/admin of the authenticated organization. A key cannot revoke another organization's key or its own parent or any transitive ancestor; ancestor revocation returns 403 key_ancestor_revocation_forbidden. Revoked ancestors remain in the delegation chain. Keys created before parent relationships were recorded have no recoverable historical ancestry.
Creation takes name, scopes, integer expirationDays from 1 to 360, optional nullable allowedOperations, and the existing budgetCredits, budgetPeriodDays, and maxRequestCredits bounds. Any priced scope requires finite bounds. For example, using a dedicated management key whose expiry is more than one day away:
curl https://www.spore.ee/api/v1/keys \
-H "Authorization: Bearer $MANAGEMENT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-research-agent-1" \
-d '{"name":"Research agent","scopes":["search:run"],"expirationDays":1,"allowedOperations":["search"],"budgetCredits":100,"budgetPeriodDays":1,"maxRequestCredits":10}'The management caller must have no spending bounds: any per-request bound, rolling budget, or budget period refuses minting, so a leaked capped key cannot multiply its budget by creating children. An unbounded management key may grant requested scopes, including priced scopes with finite bounds; it is administrative authority, not a spending credential. Every child expires no later than its parent. A restricted parent must grant an explicit subset of its own Operation allowlist and cannot grant key:manage to a child.
The created secret is returned only on initial success. An idempotency replay returns 409, never the secret. Store it immediately; if lost, revoke that key and mint a replacement with a new idempotency key.
curl -X POST "https://www.spore.ee/api/v1/keys/$KEY_ID/revoke" \
-H "Authorization: Bearer $MANAGEMENT_KEY" \
-H "Idempotency-Key: revoke-research-agent-1"Revoking an already revoked non-ancestor key succeeds without another policy change. Revocation blocks subsequent authentication; it is not cancellation of already admitted work.
Spend bounds
Bounds let you delegate work without handing an agent the whole balance. Any key with a priced scope must carry finite credit bounds.
maxRequestCredits caps the quoted cost of one request. budgetCredits caps net burn (debits minus refunds attributed to this key) over the preceding budgetPeriodDays days, from 1 to 365. Headroom returns as spend leaves that rolling window or is refunded. This is not a lifetime credit cap. A zero bound allows no positive-priced quote. Organization credit balance and other admission rules still apply independently.
Both refusals are 402 and carry the numbers that produced them:
{
"error": "request_cap_exceeded",
"message": "This call is quoted above this key's per-request cap.",
"bound": 500,
"required": 900
}{
"error": "key_budget_exhausted",
"message": "This key has insufficient rolling-window headroom.",
"bound": 5000,
"available": 120,
"required": 400,
"periodDays": 30
}required is the quoted cost of the refused call, bound the ceiling, available the remaining headroom, and periodDays the rolling window. A bound is only checked when set.
Quoting happens before work starts, so a refusal never leaves a half-run request behind and never charges anything.
Attribution
Every credit change records the key that caused it. GET /api/v1/wallet/transactions pages the ledger newest-first by opaque cursor, and each row carries apiKeyId — null for spend driven by a dashboard session or by the system.
curl "https://www.spore.ee/api/v1/wallet/transactions?apiKeyId=$KEY_ID&limit=50" \
-H "Authorization: Bearer $SPORE_API_KEY"Per-key burn is a filter, not a join. The ledger read is free, org-wide, and cursor-paged.