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.
Before you start
assets:read to inspect and assets:write to upload or replace.STEADYLINK_API_KEY in your server environment. Do not send it to a browser, mobile app, repository, or build output.listBuckets(). The ID selects the destination for new files.@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.
npm install @steadylink/sdkimport { 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");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 replacementsconst result = await steadylink.replace(hero.assetId, await fileFromPath("./public/campaign-hero-v2.webp"));
console.log(result.version, result.url); // new revision number, same linksteadylink.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 againimport { 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.
pip install steadylinkimport 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)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"])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).
npx steadylink login
npx steadylink upload ./public/media --bucket marketing --folder campaign --public
# campaign/hero.webp https://cdn.steadylink.io/a/3f2a...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 --yessteadylink inspect "$ASSET_ID"
steadylink versions "$ASSET_ID"
steadylink rollback "$ASSET_ID" 3
steadylink focal "$ASSET_ID" --x 0.42 --y 0.31GitHub 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.
- 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
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=75Retries 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.
Need another path?