SDKs and CLI

Start with the workflow you need: send a file, replace a live revision without changing its link, mint a signed link, or ship files from CI. The TypeScript SDK, CLI, GitHub Action, and next/image loader all use the same API as the dashboard.

Pick the right tool

Use TypeScript or Python in trusted server code. Use the CLI for a local folder, the GitHub Action for a workflow, and the next/image loader to serve resized images from stable links. For a browser upload, your server creates the session and the browser only receives the temporary upload URL.

Upload API

Before you start

1. Create a keyCreate a scoped workspace key in Developers. Use assets:read to inspect and assets:write to upload or replace.
2. Keep it server-sidePut STEADYLINK_API_KEY in your server environment. Do not send it to a browser, mobile app, repository, or build output.
3. Find the bucket IDOpen a bucket in the dashboard or call listBuckets(). The ID selects the destination for new files.
Published packages: the TypeScript client is available as @steadylink/sdk on npm, the CLI as @steadylink/cli, and the Python client is available as steadylink on PyPI.

TypeScript

Use the TypeScript client in Node 18+, edge runtimes, or the browser: anything with fetch. It ships as ESM with types and requires exactly one credential: an API key or a short-lived user access token.

Install from npm
npm install @steadylink/sdk
Create a client and list buckets
import { SteadyLink } from "@steadylink/sdk";

const steadylink = new SteadyLink({
  apiKey: process.env.STEADYLINK_API_KEY!,
  // workspaceId is optional for an API key. Keys already belong to one workspace.
  timeoutMs: 30_000,
  maxRetries: 2,
});

const { items: buckets } = await steadylink.listBuckets();
const bucket = buckets.find((item) => item.slug === "marketing");
if (!bucket) throw new Error("Marketing bucket not found");
Upload files and get their links
import { fileFromPath } from "@steadylink/sdk/node";

// Streams from disk, so large files are never buffered in memory.
const [hero] = await steadylink.upload(bucket.id, await fileFromPath("./public/campaign-hero.webp"), {
  folder: "campaign",
  visibility: "public",
  onProgress: ({ loaded, total }) => console.log(Math.round((loaded / total) * 100) + "%"),
});

console.log(hero.url); // https://cdn.steadylink.io/a/{asset_id}, stable across replacements
Replace a live file without changing its URL
const result = await steadylink.replace(hero.assetId, await fileFromPath("./public/campaign-hero-v2.webp"));

console.log(result.version, result.url); // new revision number, same link
Signed links, transforms, and rollback
steadylink.link(hero.assetId, { width: 1200, format: "webp", quality: 80 });

const signed = await steadylink.createSignedLink(hero.assetId, { ttlSeconds: 3600 });
console.log(signed.url); // expiring, revocable link for a private file

await steadylink.rollback(hero.assetId, 1); // make revision 1 current again
Handle an error with its request ID
import { SteadyLinkError, SteadyLinkNetworkError } from "@steadylink/sdk";

try {
  await steadylink.listBuckets();
} catch (error) {
  if (error instanceof SteadyLinkError) {
    console.error(error.status, error.code, error.requestId, error.detail);
  } else if (error instanceof SteadyLinkNetworkError) {
    console.error(error.message);
  }
}

Python

The Python client is synchronous and has no runtime dependencies. Its upload and replacement helpers create the session, send bytes to the presigned URL, then finalize the session for you.

Install from PyPI
pip install steadylink
Create a client and upload a file
import os
from pathlib import Path
from steadylink import SteadyLink

client = SteadyLink(
    api_key=os.environ["STEADYLINK_API_KEY"],
    max_retries=2,
)

buckets = client.list_buckets()["items"]
bucket = next(item for item in buckets if item.get("slug") == "marketing")

file_path = Path("campaign-hero.webp")
upload = client.upload_file(
    bucket["id"],
    filename=file_path.name,
    body=file_path.read_bytes(),
    content_type="image/webp",
    path="campaign/",
)

print(upload)
Replace a file
replacement = Path("campaign-hero-v2.webp")

result = client.replace_file(
    bucket["id"],
    "campaign/campaign-hero.webp",
    filename=replacement.name,
    body=replacement.read_bytes(),
    content_type="image/webp",
)

print(result["version"])
Handle API and network failures
from steadylink import SteadyLinkError, SteadyLinkNetworkError

try:
    client.list_buckets()
except SteadyLinkError as error:
    print(error.status, error.code, error.request_id, error.detail)
except SteadyLinkNetworkError as error:
    print(str(error))

CLI

Run it with npx steadylink or install @steadylink/cli globally. login saves your key; STEADYLINK_API_KEY or --api-key work too. Every command accepts --json and returns scriptable exit codes (0 success, 2 usage, 3 credentials, 4 partial upload).

Log in and upload files or folders
npx steadylink login
npx steadylink upload ./public/media --bucket marketing --folder campaign --public
# campaign/hero.webp  https://cdn.steadylink.io/a/3f2a...
Replace, link, and clean up
steadylink replace "$ASSET_ID" ./campaign-hero-v2.webp
steadylink replace marketing:campaign/hero.webp ./campaign-hero-v2.webp

steadylink link "$ASSET_ID" --w 1200 --fm webp
steadylink link "$ASSET_ID" --signed --ttl 86400
steadylink ls marketing:campaign/
steadylink rm marketing:campaign/old.webp --yes
Inspect, roll back, and set a focal point
steadylink inspect "$ASSET_ID"
steadylink versions "$ASSET_ID"
steadylink rollback "$ASSET_ID" 3
steadylink focal "$ASSET_ID" --x 0.42 --y 0.31
The CLI skips symbolic links, keeps relative folder paths, uploads in batches of 100, streams large files without buffering them, and uses an idempotency key when it finalizes each file.

GitHub Action

Upload release files or replace a published download from a workflow. The action outputs link, links, and results, and adds a table of links to the job summary.

.github/workflows/release.yml
- uses: SteadyLink-io/upload-action@v1
  id: steadylink
  with:
    api-key: ${{ secrets.STEADYLINK_API_KEY }}
    bucket: releases
    files: dist/*.zip
    # replace: releases:downloads/app-latest.zip   # keep the same link instead

- run: echo "${{ steps.steadylink.outputs.link }}"

next/image loader

A Cloudinary or UploadThing alternative where the link never breaks: keep the asset ID in your code or CMS, replace the file whenever you like, and next/image keeps requesting the same /a/{asset_id} URL with w, q, and fm set per breakpoint.

steadylink-loader.js and next.config.js
// steadylink-loader.js
export { default } from "@steadylink/sdk/next-loader";

// next.config.js
module.exports = { images: { loader: "custom", loaderFile: "./steadylink-loader.js" } };

// page.tsx
<Image src="$ASSET_ID" alt="Launch hero" width={1200} height={630} />
// -> https://cdn.steadylink.io/a/$ASSET_ID?w=1200&fm=webp&q=75

Retries and browser uploads

Both clients retry 429, 502, 503, and 504 for safe reads. A write retries only when it includes an idempotency key. The upload helpers do this for completion automatically.

For a browser upload, call Create upload batch from your server, return only the session's uploadUrl to the browser, then complete the session from your server. The presigned URL is specifically designed for that direct byte transfer.