Skip to content

REST API

Base URL, authentication headers, request and response conventions, pagination, idempotency, request IDs, and an index of every REST API resource.

On this page

The SteadyLink REST API is the same API the dashboard uses. Anything you can do to buckets, files, uploads, revisions, and links in the dashboard, you can do from a server, a script, or a CI job. This page covers the rules every endpoint shares. Read it once, make the first request below, then jump to the resource you need.

Base URL#

All management routes live under one origin and the /api prefix:

Text
https://api.steadylink.io/api

Files are delivered from a separate origin that never needs an API key:

Text
https://cdn.steadylink.io/a/{asset_id}

Keep the two apart in your code. The API origin is for managing content with a credential. The delivery origin is what you put in <img> tags, emails, and QR codes. See Delivery and signed links.

Authentication#

Server integrations send a workspace API key in the X-API-Key header:

Terminal
curl https://api.steadylink.io/api/assets/ \
  -H "X-API-Key: $STEADYLINK_API_KEY"
HeaderWhen to send it
X-API-KeyEvery request from a server integration. The key is bound to one workspace and carries its own scopes.
Authorization: Bearer <token>Requests made with a signed-in user session, such as the dashboard. Not for integrations.
X-Workspace-IdSelects a workspace for a user session that belongs to more than one workspace. With an API key it is optional; if you send it, it must match the key's workspace or the request returns 403.

Send exactly one credential. A request that carries both Authorization and X-API-Key is rejected with 400 and the message Use either bearer authentication or an API key, not both. A user session that belongs to several workspaces and omits X-Workspace-Id on a workspace-level route gets 400 with the code workspace_required.

Each key has scopes, and the scope a request needs is decided by its path and method: reads under /api/assets need assets:read, writes need assets:write, and so on. Authentication lists every scope, plan limit, and rate-limit header.

Make your first request#

List the buckets in your workspace. Create a key first in Dashboard > Developers > New API key with the assets:read scope, and export it as STEADYLINK_API_KEY.

curl "https://api.steadylink.io/api/assets/?limit=20" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "items": [
    {
      "id": "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15",
      "name": "Marketing",
      "slug": "marketing",
      "kind": "bucket",
      "createdAt": "2026-08-17T18:31:00.412093",
      "isPrivate": false
    }
  ],
  "nextCursor": "2026-08-17T18:31:00.412093"
}

If you get 401, the key is missing, mistyped, expired, or revoked. If you get 403 with insufficient_scope, the key exists but lacks the scope named in message.required.

Without an SDK, any HTTP client works. This is the same request with fetch and with requests:

const response = await fetch("https://api.steadylink.io/api/assets/?limit=20", {
  headers: { "X-API-Key": process.env.STEADYLINK_API_KEY! },
});
const body = await response.json();
if (!response.ok) {
  throw new Error(`${response.status} ${JSON.stringify(body.message)} (request ${body.requestId})`);
}
const { items, nextCursor } = body;

Requests and responses#

JSON bodies use camelCase. Request bodies and response fields are camelCase: bucketId, contentType, expiresInDays, objectAssetId, nextCursor. Send bodies with Content-Type: application/json.

Many inputs are query parameters, even on POST. Several file operations take their inputs in the query string, and those names are snake_case because they mirror the server's parameters: from_key, to_key, upload_temp_key, content_type, bucket_id. Each operation's reference lists exactly where every parameter goes. When in doubt, URL-encode keys and paths: a file key such as campaign/launch/hero image.webp must be sent as campaign%2Flaunch%2Fhero%20image.webp.

IDs are UUIDs. Buckets, files, upload sessions, revisions, and grants are identified by UUID strings. A file's UUID is its asset ID, the value in its delivery URL.

Timestamps are UTC without an offset. Fields such as createdAt and expiresAt look like 2026-08-17T18:31:00.412093. Treat them as UTC.

Successful writes return small confirmations. Many writes return a flag such as {"deleted": true} or {"renamed": true} rather than the updated resource. Read the resource again if you need its new state.

Pagination#

List endpoints that can grow without bound use cursor pagination. Pass limit and, for the next page, the nextCursor value from the previous response as cursor.

EndpointlimitCursor behavior
List buckets1 to 100, default 20nextCursor is set whenever the page has items, including the last page. Stop when a page returns fewer items than limit or no items.
List every file in the workspace1 to 1000, default 1000nextCursor is null on the last page. An invalid cursor returns 422.

Other lists return everything in one response: a folder listing, a file's revisions, and a file's signed links (the 200 most recent).

Read every bucket
let cursor: string | undefined;
const buckets = [];
while (true) {
  const page = await steadylink.listBuckets(100, cursor);
  buckets.push(...page.items);
  if (page.items.length < 100 || !page.nextCursor) break;
  cursor = page.nextCursor;
}

Idempotency#

Completing an upload session is the one operation with an explicit Idempotency-Key contract. Send a key that is unique to the session, such as complete-{session_id}, and reuse the same key when you retry. A different key for a session that already recorded one returns 409.

The TypeScript and Python clients only retry a write automatically when it carries an Idempotency-Key. Reads (GET, HEAD, OPTIONS) are retried on 429, 502, 503, and 504.

Request IDs#

Every response carries an X-Request-Id header. You can send your own value (ASCII, at most 128 characters) to correlate logs across systems. Anything else is replaced with a generated UUID. Error bodies repeat the same value as requestId:

403 ForbiddenError response
{
  "code": 403,
  "message": {
    "code": "insufficient_scope",
    "required": "assets:write"
  },
  "requestId": "f3b6a0c2-91d4-4e7b-8a5c-2d9e1f0b7c34"
}

message is a string for simple errors and an object with a machine-readable code for errors you are expected to handle. Request bodies that fail validation return 400 with the message Validation error. Include the request ID when you contact support. Never include the API key or a signed token. The full list of codes is in Errors.

Rate limits#

API-key requests are limited per workspace, not per key: a short burst window, a per-minute read or write window, a monthly operation allowance, and a separate allowance for expensive operations such as uploads, replacements, and signed links. Successful API-key responses include RateLimit, RateLimit-Policy, and X-RateLimit-* headers. A limited request returns 429 with Retry-After in seconds. Plan values and header formats are in Authentication.

Resources#

Next steps#