Authentication
Create, scope, expire, and revoke workspace API keys, and understand the plan limits and rate-limit headers that apply to every key.
On this page
Use this page to give an integration exactly the access it needs. You will create a workspace API key, choose its scopes and expiry, learn how much traffic your plan allows, read the rate-limit headers on each response, and revoke the key when it is no longer needed.
How requests authenticate#
SteadyLink accepts two kinds of credentials. Integrations should always use an API key.
| Credential | Header | Who uses it | Workspace |
|---|---|---|---|
| Workspace API key | X-API-Key: slk_... | Servers, scripts, CI jobs, the SDKs, the CLI, and the GitHub Action | Fixed: a key belongs to exactly one workspace |
| User session token | Authorization: Bearer <token> | The dashboard, on behalf of a signed-in person | Chosen with X-Workspace-Id when the person belongs to several workspaces |
Send one or the other. A request with both headers returns 400. With an API key, X-Workspace-Id is optional and must match the key's workspace if you send it; a different workspace returns 403.
An API key acts on behalf of the workspace, not a person. It is limited by its scopes, not by the role of the person who created it. A session token acts as the person and is limited by their workspace role.
When a key is checked, SteadyLink does the following, in order, and stops at the first failure:
- Looks up the key. Unknown keys return
401 Invalid API key. - Rejects revoked and expired keys with
401 API key expired or revoked. - Checks that the workspace's plan allows API access. Paused access returns
403withsubscription_inactiveorapi_access_disabled. - Applies the workspace's rate limits. A limit returns
429. - Checks the route's required scope. A missing scope returns
403withinsufficient_scope. - Runs the route inside the key's workspace. Resources in other workspaces return
403or404.
Create an API key#
Most people create keys in the dashboard: open Dashboard > Developers, choose New API key, name it after the system that will use it, and select its scopes. The raw key is shown once. SteadyLink stores only a hash, so a lost key cannot be recovered; revoke it and create another.
Keys start with slk_. Store the value in your server's secret manager or environment, for example as STEADYLINK_API_KEY.
Key management routes act on behalf of a person. They require a signed-in session of a workspace owner or admin and reject API keys, even keys that hold workspace:admin. An integration cannot mint or revoke keys for itself.
Create an API key#
/api/workspaces/current/api-keysBody
namestringRequired- Label shown in the dashboard, 1 to 200 characters. Name it after the integration, such as
production uploader. scopesstring[]Default["assets:read", "assets:write"]- One or more of the scopes below. Duplicates are ignored. An empty list or an unknown scope returns
422with the codeinvalid_scopesand the list of allowed values. expiresInDaysinteger- Days until the key stops working, 1 to 365. Omit it for a key that does not expire.
curl -X POST "https://api.steadylink.io/api/workspaces/current/api-keys" \
-H "Authorization: Bearer $STEADYLINK_SESSION_TOKEN" \
-H "X-Workspace-Id: 0d9b3f6e-2a71-4c58-b8e4-5f1a7c3d9e20" \
-H "Content-Type: application/json" \
-d '{
"name": "production uploader",
"scopes": ["assets:read", "assets:write"],
"expiresInDays": 90
}'{
"id": "6a2f9d4c-1e8b-4f37-a5c0-9b3d7e2f1a68",
"name": "production uploader",
"scopes": ["assets:read", "assets:write"],
"createdAt": "2026-10-08T14:22:31.482913",
"lastUsedAt": null,
"expiresAt": "2027-01-06T14:22:31.482913",
"revokedAt": null,
"key": "slk_9QxV2mT7c4LrW8pZ1nKe5bYh3sJa6dGf0uHo"
}| Status | Code | Meaning |
|---|---|---|
403 | insufficient_workspace_permission | The person is not an owner or admin, or the request used an API key. |
403 | subscription_inactive, api_access_disabled | The workspace cannot use the API right now. See Plan access. |
409 | api_key_limit_reached | The workspace already has as many active keys as its plan allows. Revoke one first. |
422 | invalid_scopes | The scope list is empty or contains a value that is not a key scope. |
List API keys#
/api/workspaces/current/api-keysReturns every key in the workspace, newest first, including revoked and expired keys. Raw key values are never returned after creation.
{
"items": [
{
"id": "6a2f9d4c-1e8b-4f37-a5c0-9b3d7e2f1a68",
"name": "production uploader",
"scopes": ["assets:read", "assets:write"],
"createdAt": "2026-10-08T14:22:31.482913",
"lastUsedAt": "2026-10-08T15:02:10.118204",
"expiresAt": "2027-01-06T14:22:31.482913",
"revokedAt": null
}
]
}lastUsedAt is refreshed at most once every five minutes, so treat it as approximate. It is useful for finding keys that nothing uses anymore.
Revoke an API key#
/api/workspaces/current/api-keys/{key_id}Revocation takes effect on the next request: anything using the key starts receiving 401 API key expired or revoked. The key stays in the list with revokedAt set, for your audit trail. A key ID from another workspace returns 404.
{ "revoked": true }To rotate a key without downtime, create the new key, deploy it, confirm it works (its lastUsedAt appears), then revoke the old one. On the Free plan you can hold only one active key, so rotation means revoking first and deploying the new key immediately after.
Scopes#
Pick the smallest set that covers what the integration does. A key that only reads delivery statistics should not be able to delete files.
| Scope | Dashboard label | Grants |
|---|---|---|
assets:read | Read files | Read buckets, folders, files, revisions, upload status, upload events, signed-link records, and conversion jobs. |
assets:write | Change files | Create and change buckets, folders, files, uploads, replacements, revisions, signed links, collections, widgets, and conversions. |
analytics:read | Read analytics | Read traffic, top files, traffic sources, storage, and activity analytics. |
workspace:read | Read workspace | Read workspace details, members, invitations, platform settings, and the API entitlement. |
workspace:admin | Manage workspace | Write access to /api/platform/... routes (delivery policy, focal points, migrations, domains, webhooks, SSO). Workspace settings, members, invitations, and API keys still require a signed-in owner or admin session. |
These five are the only scopes a key can hold. Role permissions such as billing management and ownership transfer are never available to keys.
How a route's scope is decided#
The required scope comes from the request path and method, so you can predict it for any route:
| Path | GET, HEAD, OPTIONS | Other methods |
|---|---|---|
/api/analytics/... | analytics:read | analytics:read |
/api/operations/... | analytics:read | workspace:admin |
/api/workspaces/..., /api/orgs/..., /api/platform/... | workspace:read | workspace:admin |
Everything else, including /api/assets/..., /api/upload-batches, and /api/upload-sessions/... | assets:read | assets:write |
A key without the required scope gets:
{
"code": 403,
"message": { "code": "insufficient_scope", "required": "assets:write" },
"requestId": "0b7e2c94-5a1d-4f63-9e8b-c2d4f6a8b013"
}Some routes act on behalf of a person and reject keys regardless of scope, for example API-key management above and personal notification preferences.
Expiry#
A key created with expiresInDays stops working at expiresAt and returns 401 API key expired or revoked, exactly like a revoked key. Nothing warns the integration in advance, so record the expiry date where your team will see it, or check expiresAt from the key list on a schedule. Keys without an expiry work until revoked. Expired keys no longer count toward the plan's active-key limit.
Plan access#
Every workspace can use the API, including Free. All limits below belong to the workspace and are shared by all of its keys.
| Plan | Active keys | Monthly operations | Reads per minute | Writes per minute | Burst per 10 s | Expensive ops per minute / day | Largest upload through the API |
|---|---|---|---|---|---|---|---|
| Free | 1 | 50,000 | 120 | 30 | 30 | 10 / 250 | 100 MiB |
| Personal | 2 | 250,000 | 300 | 75 | 75 | 25 / 2,500 | 250 MiB |
| Pro | 10 | 2,000,000 | 1,200 | 300 | 300 | 120 / 25,000 | 500 MiB |
| Business | 50 | 25,000,000 | 6,000 | 1,500 | 1,250 | 600 / 250,000 | 2 GiB |
| Enterprise | 100 | 2,000,000,000 | 60,000 | 15,000 | 10,000 | 6,000 / 5,000,000 | 500 MiB |
Free includes one active, scoped API key and 50,000 monthly management operations, enough to build and test a real integration. Each authenticated API-key request counts as one management operation. Delivery through cdn.steadylink.io is not a management operation and is not limited by these numbers.
Largest upload is the lower of two limits: the API ceiling for the plan and the workspace's own upload limit. The values above are the effective result.
Expensive operations are writes whose path contains /upload, /commit, /replace, /purge, /signed-url, or /retry, plus every write under /api/conversions. Creating an upload batch, completing an upload session, finishing a replacement, and creating a signed link all count. These have their own per-minute and per-day ceilings so that one busy integration cannot exhaust scanning and storage capacity for the rest of the workspace.
Paid plans need active billing. Personal, Pro, Business, and Enterprise limits apply only while the subscription is active. If billing lapses or the billing period ends without renewal, API-key requests return 403 with the code subscription_inactive. Keys are not deleted: owners can still list and revoke them in the dashboard, and the keys work again as soon as billing is active. api_access_disabled means API access was turned off for the workspace by an administrative override; contact support.
Check the current state from code with Get the API entitlement:
curl https://api.steadylink.io/api/workspaces/current/api-entitlement \
-H "X-API-Key: $STEADYLINK_API_KEY"{
"enabled": true,
"reason": null,
"plan": "pro",
"billingStatus": "active",
"currentPeriodEnd": "2026-11-01T00:00:00",
"limits": {
"readRequestsPerMinute": 1200,
"writeRequestsPerMinute": 300,
"burstRequestsPer10Seconds": 300,
"expensiveOperationsPerMinute": 120,
"expensiveOperationsPerDay": 25000,
"requestsPerMonth": 2000000,
"activeApiKeys": 10,
"maxUploadBytes": 5368709120
},
"activeApiKeys": 2
}limits.maxUploadBytes is the plan's API ceiling on its own. The effective largest upload is the smaller of that value and the workspace upload limit, as in the table above. This route needs workspace:read.
Rate limits#
Limits are pooled by workspace. Five keys in one workspace share one allowance; creating more keys does not add capacity.
Each API-key request is checked against, in order:
- The burst window: requests per 10 seconds, reads and writes together.
- The per-minute window for its category.
GET,HEAD, andOPTIONSare reads; everything else is a write. - The monthly operation allowance.
- For expensive operations, the per-minute and per-day expensive allowances.
Successful API-key responses describe the per-minute window for the request's category:
| Header | Example | Meaning |
|---|---|---|
RateLimit | limit=1200, remaining=1187, reset=42 | Limit, remaining requests, and seconds until the window resets. |
RateLimit-Policy | "steadylink";q=1200;w=60 | The quota and window length in seconds. |
X-RateLimit-Limit | 1200 | Same limit, legacy header. |
X-RateLimit-Remaining | 1187 | Same remaining count, legacy header. |
X-RateLimit-Reset | 1791469380 | When the window resets, as a Unix timestamp in seconds. |
A request over any limit returns 429 with Retry-After in seconds and a code that names the limit:
{
"code": 429,
"message": {
"code": "api_write_limit_exceeded",
"message": "Rate limit exceeded",
"retryAfter": 18
},
"requestId": "4e1a8c63-0f2b-4d97-b5e8-71c3a9d0f246"
}| Code | Limit reached |
|---|---|
api_burst_limit_exceeded | Burst window (10 seconds) |
api_read_limit_exceeded | Reads per minute |
api_write_limit_exceeded | Writes per minute |
api_monthly_quota_exceeded | Monthly operations |
api_expensive_operation_rate_exceeded | Expensive operations per minute |
api_expensive_operation_limit_exceeded | Expensive operations per day |
invalid_api_key_attempt_limit_exceeded | Too many requests with unknown, expired, or revoked keys from one client address (30 per minute) |
Wait for Retry-After before retrying, and add jitter if several workers share a key. The SDKs already do this for reads and idempotent writes. A monthly or daily limit will not clear in seconds; reduce traffic or upgrade instead of retrying in a loop.