Authentication
Spooled uses API keys for authentication. Each key belongs to one organization and can be restricted to specific queues.
Quick links:
API Keys
API keys authenticate all requests to Spooled. Create a separate key per service, so each one can be restricted to the queues it uses and revoked on its own.
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#ecfdf5', 'primaryTextColor': '#065f46', 'primaryBorderColor': '#10b981', 'lineColor': '#6b7280'}}}%%
sequenceDiagram
participant App as Your App
participant S as Spooled API
App->>S: Request with API Key
Note right of S: Authorization: Bearer sp_live_...
S->>S: Validate key
S-->>App: Response Key Format
| Prefix | Environment | Example |
|---|---|---|
sp_live_ | Production | sp_live_abc123def456... |
sp_test_ | Test/Development | sp_test_xyz789uvw012... |
Dashboard Tip
What to look for:
- → Key name and creation date
- → Last used timestamp (coarse — written at most once every 5 minutes)
- → Queue scope (All queues, or the listed queues)
Actions:
- Create new keys
- Revoke compromised keys
- Rotate keys regularly
Using API Keys
For REST, include your API key or JWT in the Authorization: Bearer … header
(JWT tokens are detected by an eyJ prefix). The header is the only transport REST accepts:
do not use an X-API-Key header, and do not put the credential in the query string.
# All API requests require authentication
curl -X POST https://api.spooled.cloud/api/v1/jobs \
-H "Authorization: Bearer sp_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"queue_name": "my-queue", "payload": {...}}'Query-string credentials are rejected on REST
A REST request carrying ?api_key=… or ?token=… returns
401 Unauthorized. Move the credential into the Authorization: Bearer …
header — nothing else changes about the request. Query strings are recorded by reverse-proxy and
CDN access logs, tracing spans, browser history and the Referer header, and API keys
do not expire by default, so a key that lands in any of those stays usable indefinitely
(CWE-598).
Four realtime routes still accept the credential in the query string, because browser
EventSource and WebSocket clients cannot set headers:
/api/v1/ws, /api/v1/events,
/api/v1/events/jobs/{id} and
/api/v1/events/queues/{name}. See
Real-time Updates. Everywhere else, use the header.
For gRPC, pass the API key only in metadata as x-api-key — the gRPC path does not accept JWT.
x-api-key: sp_live_your_api_key Environment Variables
Never hardcode API keys. Use environment variables to keep them secure.
# .env file (add to .gitignore!)
SPOOLED_API_KEY=sp_live_your_api_key
# Use in shell
export SPOOLED_API_KEY=sp_live_your_api_key
curl -H "Authorization: Bearer $SPOOLED_API_KEY" ...Security Warning
- • Never commit API keys to version control
- • Add
.envto your.gitignore - • Use different keys for development and production
- • Rotate keys periodically
Creating API Keys
Create keys via the dashboard or API:
# Create a new API key
curl -X POST https://api.spooled.cloud/api/v1/api-keys \
-H "Authorization: Bearer sp_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Email worker",
"queues": ["emails"]
}'
# Response (key shown only once!)
# Omit "queues" (or send []) for a key that can reach every queue.
{
"id": "key_xxxxx",
"key": "sp_live_a1b2c3d4...",
"name": "Email worker",
"queues": ["emails"],
"created_at": "2025-01-15T10:30:00Z",
"expires_at": null
}Restricting a key
Spooled keys have no per-permission scopes (there is no read-only key): every key
can call every API route of its organization. The way to limit a key is its
queue scope. Pass queues when creating or updating a key, and it can only
enqueue, claim, read and manage jobs in those queues:
queues | What the key can reach |
|---|---|
omitted, [] or ["*"] | All queues — plus organization-wide routes such as API keys, billing and settings |
["emails", "billing"] | Only jobs, workers, schedules and events in emails and billing. It cannot
manage API keys or organization settings, and cannot mint a key broader than itself. |
["orders.*"] | Every queue whose name starts with orders. (for example orders.eu and
orders.eu.west), including queues created later. It does not cover orders itself:
list that name too if you need it. * is only valid alone or as a trailing .* after
a prefix; orders* and *.eu are rejected. |
One queue per location, keys by prefix
For a tenant, store, restaurant or customer, give each location one queue and name the queues with a
shared prefix: orders.store-42, orders.store-43. A key scoped to
["orders.store-42"] reaches only that location, so a leaked key exposes one site. A key scoped to
["orders.*"] reaches every location, including ones created later: use it for a service that works for
many locations (a spooler, a dispatcher) and keep it on trusted machines.
Do the routing inside a location with the payload or the job type, for example a printer field for
which printer in the kitchen should print a ticket, rather than with one queue per printer or device. Every queue counts
against the plan's queue cap, and so do API keys and workers, so a queue per printer runs out of queues long before a
queue per location does. Size the plan to the number of locations (see
Rate Limits & Quotas); queue-prefix keys change how you scope access,
not what the plan allows.
A permissions field is rejected
Older versions of this page described jobs:read-style permissions. The API never
enforced them, so a key created with "permissions": ["jobs:read"] had full access.
Since API 0.1.114, sending permissions returns 400 VALIDATION_ERROR instead of
silently creating that key. Use queues, and give each service its own key so a leaked
one can be revoked without touching the rest.
Key Rotation
Rotate API keys periodically for security. The recommended approach:
- Create a new API key with the same queue scope
- Update your services to use the new key
- Verify everything works with the new key
- Revoke the old key
API Key Issues
What to look for:
- → Key status (active vs revoked)
- → Last used timestamp — updated at most once every 5 minutes, so a working key can still look idle
- → Key prefix matches environment
Actions:
- Check key hasn't been revoked
- Verify using correct environment key
- Confirm the credential is in the Authorization header, not the query string
- Generate a new key if compromised
Rate Limits
API keys are subject to per-plan rate limits. When rate limited, you'll receive a
429 Too Many Requests response with a Retry-After header telling you
how long to wait before retrying.
The exact request-per-second limits, burst allowances, and quota-enforcement behaviour are listed — as a single source of truth — on the Rate Limits & Quotas page.
Next Steps
- Manage your API keys
- Quickstart guide — Use your key to create jobs
- SDK documentation — Language-specific setup