Skip to content

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.

CredentialHeaderWho uses itWorkspace
Workspace API keyX-API-Key: slk_...Servers, scripts, CI jobs, the SDKs, the CLI, and the GitHub ActionFixed: a key belongs to exactly one workspace
User session tokenAuthorization: Bearer <token>The dashboard, on behalf of a signed-in personChosen 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:

  1. Looks up the key. Unknown keys return 401 Invalid API key.
  2. Rejects revoked and expired keys with 401 API key expired or revoked.
  3. Checks that the workspace's plan allows API access. Paused access returns 403 with subscription_inactive or api_access_disabled.
  4. Applies the workspace's rate limits. A limit returns 429.
  5. Checks the route's required scope. A missing scope returns 403 with insufficient_scope.
  6. Runs the route inside the key's workspace. Resources in other workspaces return 403 or 404.

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#

POST/api/workspaces/current/api-keys
Signed-in session of a workspace owner or admin

Body

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 422 with the code invalid_scopes and 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
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
  }'
201 CreatedResponse
{
  "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"
}
StatusCodeMeaning
403insufficient_workspace_permissionThe person is not an owner or admin, or the request used an API key.
403subscription_inactive, api_access_disabledThe workspace cannot use the API right now. See Plan access.
409api_key_limit_reachedThe workspace already has as many active keys as its plan allows. Revoke one first.
422invalid_scopesThe scope list is empty or contains a value that is not a key scope.

List API keys#

GET/api/workspaces/current/api-keys
Signed-in session of a workspace owner or admin

Returns every key in the workspace, newest first, including revoked and expired keys. Raw key values are never returned after creation.

200 OKResponse
{
  "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#

DELETE/api/workspaces/current/api-keys/{key_id}
Signed-in session of a workspace owner or admin

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.

200 OKResponse
{ "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.

ScopeDashboard labelGrants
assets:readRead filesRead buckets, folders, files, revisions, upload status, upload events, signed-link records, and conversion jobs.
assets:writeChange filesCreate and change buckets, folders, files, uploads, replacements, revisions, signed links, collections, widgets, and conversions.
analytics:readRead analyticsRead traffic, top files, traffic sources, storage, and activity analytics.
workspace:readRead workspaceRead workspace details, members, invitations, platform settings, and the API entitlement.
workspace:adminManage workspaceWrite 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:

PathGET, HEAD, OPTIONSOther methods
/api/analytics/...analytics:readanalytics:read
/api/operations/...analytics:readworkspace:admin
/api/workspaces/..., /api/orgs/..., /api/platform/...workspace:readworkspace:admin
Everything else, including /api/assets/..., /api/upload-batches, and /api/upload-sessions/...assets:readassets:write

A key without the required scope gets:

403 ForbiddenResponse
{
  "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.

PlanActive keysMonthly operationsReads per minuteWrites per minuteBurst per 10 sExpensive ops per minute / dayLargest upload through the API
Free150,000120303010 / 250100 MiB
Personal2250,000300757525 / 2,500250 MiB
Pro102,000,0001,200300300120 / 25,000500 MiB
Business5025,000,0006,0001,5001,250600 / 250,0002 GiB
Enterprise1002,000,000,00060,00015,00010,0006,000 / 5,000,000500 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
curl https://api.steadylink.io/api/workspaces/current/api-entitlement \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "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:

  1. The burst window: requests per 10 seconds, reads and writes together.
  2. The per-minute window for its category. GET, HEAD, and OPTIONS are reads; everything else is a write.
  3. The monthly operation allowance.
  4. 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:

HeaderExampleMeaning
RateLimitlimit=1200, remaining=1187, reset=42Limit, remaining requests, and seconds until the window resets.
RateLimit-Policy"steadylink";q=1200;w=60The quota and window length in seconds.
X-RateLimit-Limit1200Same limit, legacy header.
X-RateLimit-Remaining1187Same remaining count, legacy header.
X-RateLimit-Reset1791469380When 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:

429 Too Many RequestsResponse
{
  "code": 429,
  "message": {
    "code": "api_write_limit_exceeded",
    "message": "Rate limit exceeded",
    "retryAfter": 18
  },
  "requestId": "4e1a8c63-0f2b-4d97-b5e8-71c3a9d0f246"
}
CodeLimit reached
api_burst_limit_exceededBurst window (10 seconds)
api_read_limit_exceededReads per minute
api_write_limit_exceededWrites per minute
api_monthly_quota_exceededMonthly operations
api_expensive_operation_rate_exceededExpensive operations per minute
api_expensive_operation_limit_exceededExpensive operations per day
invalid_api_key_attempt_limit_exceededToo 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.

Next steps#