Skip to content

Quickstart

Upload a file, get its permanent link, replace the file, and watch the same link serve the new version. About five minutes.

On this page

In this quickstart you upload one image through the API, copy its stable link, publish a new version of the image, and confirm that the same link now serves the new bytes. Every step shows curl, the TypeScript SDK, the Python SDK, and the CLI, so pick the tool you plan to use in production. If you would rather click than type, skip to Prefer the dashboard?.

You need a terminal and an image file. The examples use hero.webp and a second version called hero-v2.webp; any JPEG, PNG, WebP, or PDF works the same way.

  1. Create an account

    Sign up at steadylink.io/signup. A workspace is created for you. The Free plan includes one active API key, which is all this guide needs.

  2. Create a scoped API key

    Open Dashboard > Developers, choose New API key, give it a name such as quickstart, and select two scopes:

    • Read files (assets:read) to list buckets and check upload status.
    • Change files (assets:write) to create buckets, upload, and replace.

    The full key starts with slk_ and is shown once. Copy it into an environment variable in the terminal you will use:

    Terminal
    export STEADYLINK_API_KEY="slk_..."

    The key belongs to one workspace, so you never pass a workspace ID with it. Keep it on servers and in CI secrets; never put it in browser code. To let people upload from a web page, see Browser uploads.

  3. Install a client (optional)

    curl needs nothing else. The JSON parsing in the curl examples uses jq.

    npm install @steadylink/sdk

    The CLI also reads STEADYLINK_API_KEY directly, so login is optional when the variable is set.

Find or create a bucket#

A bucket is the top-level container your files live in. Every upload names a destination bucket by its ID. List the buckets in your workspace first; a new workspace has none until you create one or upload from the dashboard (which creates My files).

curl https://api.steadylink.io/api/assets/ \
  -H "X-API-Key: $STEADYLINK_API_KEY"

If the list is empty, create a bucket. The slug is optional; the SDK and CLI let you refer to a bucket by ID, slug, or name.

curl -X POST https://api.steadylink.io/api/assets/ \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Marketing", "slug": "marketing"}'
201 CreatedResponse
{ "id": "7c1d5e2a-4b8f-4f3a-9e61-2d0c8a5b3f17" }

The CLI cannot create buckets; use one of the other tools or the dashboard. Save the ID for the next steps:

Terminal
export BUCKET_ID="7c1d5e2a-4b8f-4f3a-9e61-2d0c8a5b3f17"

Upload a file#

An upload has three parts: ask SteadyLink for an upload session, send the bytes straight to storage with the presigned URL it returns, then tell SteadyLink you are done so it can finalize the file and scan it for malware. The SDKs and CLI do all three for you.

# 1. Create an upload batch with one file. size is required.
SIZE=$(wc -c < hero.webp | tr -d ' ')
BATCH=$(curl -s -X POST https://api.steadylink.io/api/upload-batches \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"bucketId\": \"$BUCKET_ID\", \"files\": [{\"filename\": \"hero.webp\", \"contentType\": \"image/webp\", \"size\": $SIZE}]}")
BATCH_ID=$(echo "$BATCH" | jq -r '.id')
SESSION_ID=$(echo "$BATCH" | jq -r '.files[0].id')
UPLOAD_URL=$(echo "$BATCH" | jq -r '.files[0].uploadUrl')

# 2. PUT the raw bytes to storage. The URL is signed for this exact Content-Type.
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @hero.webp

# 3. Complete the session. Reuse the same Idempotency-Key if you retry.
curl -X POST "https://api.steadylink.io/api/upload-sessions/$SESSION_ID/complete" \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Idempotency-Key: complete-$SESSION_ID"

# 4. Poll until the file is ready (finalized and scanned).
curl "https://api.steadylink.io/api/upload-batches/$BATCH_ID" \
  -H "X-API-Key: $STEADYLINK_API_KEY"

The completion call returns 202 Accepted with the session in committing. Polling the batch shows it move through scanning to ready:

200 OKResponse
{
  "id": "0b9e7d4c-2f1a-4c6b-8e3d-9a7f5b1c2d40",
  "status": "complete",
  "totalFiles": 1,
  "completedFiles": 1,
  "failedFiles": 0,
  "files": [
    {
      "id": "5a8c3e1f-6d2b-4f9a-b7c0-1e2d3f4a5b6c",
      "batchId": "0b9e7d4c-2f1a-4c6b-8e3d-9a7f5b1c2d40",
      "bucketId": "7c1d5e2a-4b8f-4f3a-9e61-2d0c8a5b3f17",
      "objectAssetId": "3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b",
      "filename": "hero.webp",
      "path": "",
      "contentType": "image/webp",
      "expectedSize": 284102,
      "status": "ready",
      "revisionNumber": 1,
      "error": null,
      "expiresAt": "2026-10-09T14:02:11.402913",
      "createdAt": "2026-10-08T14:02:11.402913",
      "updatedAt": "2026-10-08T14:02:15.118204"
    }
  ]
}

objectAssetId is the file's permanent identity. If the status ends at blocked or failed, read error.code: malware_detected and policy mean the file was rejected, upload_size_mismatch means the bytes you sent do not match the size you declared. Upload and replace files lists every state.

With curl and Python, make the file public now. The JavaScript visibility option and the CLI --public flag already did this.

export ASSET_ID="3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b"
curl -X POST "https://api.steadylink.io/api/assets/$BUCKET_ID/objects/visibility?key=hero.webp&visibility=public" \
  -H "X-API-Key: $STEADYLINK_API_KEY"

The link is the delivery origin plus the asset ID:

Text
https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4e7a-a1c2-5d6e7f809a1b

Open it in a browser or fetch the headers. No API key is involved: public delivery is anonymous.

Terminal
curl -I "https://cdn.steadylink.io/a/$ASSET_ID"

This is the address you put in your website, email template, app config, or QR code. It does not contain the filename, the bucket, or the folder, so renaming or moving the file later does not change it.

Replace the file#

Now publish hero-v2.webp as the next revision of the same file. A replacement never creates a new asset ID; it appends revision 2 and makes it current.

# 1. Ask for a temporary upload URL in the file's bucket.
SIZE=$(wc -c < hero-v2.webp | tr -d ' ')
TEMP=$(curl -s -X POST "https://api.steadylink.io/api/assets/$BUCKET_ID/objects/upload-temp?size=$SIZE&content_type=image/webp" \
  -H "X-API-Key: $STEADYLINK_API_KEY")
TEMP_KEY=$(echo "$TEMP" | jq -r '.tempKey')

# 2. PUT the new bytes.
curl -X PUT "$(echo "$TEMP" | jq -r '.uploadUrl')" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @hero-v2.webp

# 3. Publish them as the next revision of the file at key hero.webp.
curl -X POST "https://api.steadylink.io/api/assets/$BUCKET_ID/objects/replace?key=hero.webp&upload_temp_key=$TEMP_KEY&original_filename=hero-v2.webp" \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{ "replaced": true, "version": 2 }

The replacement call is addressed by bucket and key (the file's path inside the bucket). The JavaScript SDK and the CLI also accept the asset ID or the link and look up the bucket and key for you.

Open the link again. Because it follows the current revision, it now serves hero-v2.webp, but there is one thing to know about timing:

  • The stable link is cacheable for five minutes (Cache-Control: public, max-age=300, stale-while-revalidate=3600). A browser or edge that fetched revision 1 recently can keep showing it until that copy expires, and may serve it once more while it refreshes in the background.
  • The new revision is also scanned for malware before it is served. A request that reaches SteadyLink before the scan finishes gets 423 Locked (pinned URLs included); retry after a moment.

To confirm the new bytes immediately, pin the revision with v. A pinned URL is a different cache entry, so nothing stale stands in the way:

Terminal
curl -I "https://cdn.steadylink.io/a/$ASSET_ID?v=2"
Text
HTTP/2 200
content-type: image/webp
cache-control: public, max-age=31536000, immutable, stale-while-revalidate=604800, stale-if-error=604800, no-transform

Use ?v= for checking, not as a cache buster in production. Any query parameter SteadyLink does not recognize, such as ?nocache=1, is rejected with 400 Unknown params, and a pinned URL never moves to later revisions. Revision 1 is still retained: ?v=1 serves it, and you can make it current again at any time (see Revisions).

Prefer the dashboard?#

Everything above can be done without code:

  1. Create a bucket

    Open Files and choose New bucket. Give it a clear name for the product, team, or campaign it belongs to. If you skip this, your first upload creates a bucket called My files.

  2. Upload files

    Choose Upload, select one or more files or drop them into the dialog, pick the destination bucket and folder, and choose who can open the link. You can close the dialog and keep working; progress moves to the activity panel.

  3. Wait for the scan

    Each file is uploaded, finalized, and then scanned for malware. A clean file is ready for delivery; a file that fails the scan is blocked and never served.

  4. Copy the link

    When the upload finishes, choose Copy link for a public file. Private files get Copy private link instead: a signed link that expires after seven days and can be revoked. You can create more signed links from the file's details later.

  5. Replace it later

    Open the file and choose Replace. The file gets a new version and its link is unchanged.

Next steps#