Delivery and signed links
Serve files from their stable link, resize and convert images with query parameters, understand caching after replacements, and share private files with revocable signed links.
On this page
This page covers what happens after a file is uploaded: how its stable link serves bytes, which query parameters resize and convert images, how long responses are cached, and how to give someone time-limited access to a private file with a signed link.
Delivery uses its own origin, https://cdn.steadylink.io, and never takes an API key. Managing signed links uses the API origin and a key like every other route.
The JavaScript examples assume const steadylink = new SteadyLink({ apiKey: process.env.STEADYLINK_API_KEY! }) and the Python examples assume client = SteadyLink(api_key=os.environ["STEADYLINK_API_KEY"]).
Endpoints#
| Operation | Method and path | Access |
|---|---|---|
| Deliver a file | GET or HEAD https://cdn.steadylink.io/a/{asset_id} | Public, or a signed token for private files |
| Create a signed link | POST /api/assets/{asset_id}/signed-url | assets:write |
| List signed links | GET /api/assets/{asset_id}/signed-urls | assets:read |
| Revoke a signed link | DELETE /api/assets/{asset_id}/signed-urls/{grant_id} | assets:write |
| Get a preview URL | GET /api/assets/object/{asset_id}/preview | assets:read |
| Purge cached image variants | POST /api/assets/{asset_id}/purge | assets:write |
Delivery#
Deliver a file#
/a/{asset_id}Serves the file's current revision, or a specific revision with v. Without parameters it returns the original bytes. With image parameters it returns a resized or converted variant, which is generated on the first request and stored for later ones. HEAD is also supported and returns the same headers without a body.
Query parameters
vinteger- Serve this revision number instead of the current one. Pinned responses are cached for a year. Omit it to follow the current revision.
tokenstring- Signed token from Create a signed link. Required for private files, ignored for public ones.
winteger- Target width in pixels, 1 to 8000. Alias:
width. hinteger- Target height in pixels, 1 to 8000. Alias:
height. fitstringDefaultcontaincoverscales and crops to exactlywbyh.contain,inside, andoutsideare accepted and currently behave the same: the image is scaled down to fit within the box, keeping its aspect ratio, and is never enlarged.fmstring- Output format:
webp,jpg(orjpeg), orpng. Alias:format. When an image is resized withoutfm, the result is JPEG, so setfm=pngorfm=webpto keep transparency. qintegerDefault82- Output quality for JPEG and WebP, 30 to 95. Values outside the range return
400. Alias:quality. On its own,qdoes not create a variant; combine it withw,h, orfm. dprnumberDefault1- Device pixel ratio, 0.1 to 3. Without
wandhit scales the original, for exampledpr=0.5halves it. Withworhit does not change the output size: request the pixel width you need directly, such asw=1200for a 600-pixel slot on a 2x screen.worhmultiplied bydprmust stay within 8000. allowUpscalebooleanDefaultfalse- For
fit=cover, allow the output to exceed twice the source dimensions. Without it, cover output is capped at 2x the source width and height. Other fits never enlarge. Acceptstrue,false,1,0,yes,no. focusstringDefaultcenterautopicks a high-detail area to keep when cropping. Requiresfit=cover.fp-x, fp-ynumber- Explicit focal point, each from 0 (left or top) to 1 (right or bottom). Send both, with
fit=cover, and withoutfocus=auto. Aliases:focal-x,focal-y. presetstring- Apply a named public transform preset from Dashboard > Developers > Transforms. Cannot be combined with other image parameters except
v. showcaseflag- Return a minimal HTML page that displays the image. Images only, and not combinable with image parameters.
ref, utm_source, utm_medium, utm_campaign, utm_content, utm_termstring- Attribution parameters. They do not affect the response and are recorded as traffic sources.
# Current revision, original bytes
https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71
# Revision 3, forever
https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71?v=3
# 1200 x 630 social card, cropped around the most detailed area, as WebP
https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71?w=1200&h=630&fit=cover&focus=auto&fm=webp
# 1200 pixels wide (a 600-pixel slot on a 2x screen) as WebP at quality 75
https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71?w=1200&fm=webp&q=75
# Private file with a signed token
https://cdn.steadylink.io/a/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71?token=v2.3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71.1791472980.1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97.4b1e...Build links in code rather than by hand:
const url = steadylink.link("3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71", {
width: 1200,
height: 630,
fit: "cover",
focus: "auto",
format: "webp",
});from urllib.parse import urlencode
asset_id = "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71"
params = {"w": 1200, "h": 630, "fit": "cover", "focus": "auto", "fm": "webp"}
url = f"https://cdn.steadylink.io/a/{asset_id}?{urlencode(params)}"steadylink link 3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71 --w 1200 --h 630 --fit cover --fm webpImage parameter rules#
- A variant is created only when
w,h,fm, or adprother than 1 is present. Other image parameters on their own return the original bytes. - Image parameters work only on raster images: JPEG, PNG, WebP, GIF, BMP, and TIFF. On any other file they return
415 Transforms supported only for raster images. SVG and PDF are served as-is. - At most eight image parameters per request, not counting
v,token, and attribution parameters. - Each parameter may appear once, and a short name cannot be combined with its alias (
wwithwidth,fmwithformat). Either returns400. - Unknown parameters return
400 Unknown params: .... A typo fails loudly instead of silently serving the original. - When both
wandhare set, their ratio must be between 1:50 and 50:1. - With
fit=coverand nofp-x,fp-y, orfocus=auto, the file's saved focal point is used if one is set. See Set a focal point. - Each file can have up to 100 distinct image variants and each workspace up to 10,000 by default. A request for a new variant beyond that returns
429withasset_transform_cardinality_exceededorworkspace_transform_cardinality_exceeded. Variants that already exist keep working. Use a small, fixed set of sizes rather than arbitrary widths.
Delivery responses#
| Status | When |
|---|---|
200 | The file or variant. |
206 | A byte range, when the request sends Range and asks for the original bytes. Use it for video and resumable downloads. |
304 | The request's If-None-Match matches the ETag. |
400 | An invalid, duplicate, conflicting, or unknown parameter. The message names the problem. |
403 | Forbidden: the file is private and the token is missing, expired, revoked, for another file, or for another revision. Blocked: infected: the current revision failed its malware scan. |
404 | No such file, a v that does not exist, or a file requested through a custom domain that belongs to another workspace. |
415 | Image parameters on a file that is not a raster image, or showcase on a file that is not an image. |
423 | Scanning: the revision's malware scan has not finished. Retry after a short wait. |
429 | Too many distinct image variants, or too many requests from one client. |
451 | The workspace's delivery policy blocks the requester's country. |
Delivery response headers#
| Header | Value |
|---|---|
Cache-Control, CDN-Cache-Control | See Caching. |
ETag | SHA-256 of the bytes served. For the original, it equals the revision's hash. |
Content-Type | The stored type, or the output type of a variant. |
Content-Disposition | inline with the stored filename. HTML and SVG files are served as attachment (downloaded, not rendered) unless the bucket allows inline rendering. |
Accept-Ranges | bytes for original files. |
Surrogate-Key, Cache-Tag | asset-{asset_id}, for targeted purges at an edge cache. |
Content-Security-Policy, X-Content-Type-Options | A strict policy and nosniff, so a hosted file cannot run scripts against your visitors. |
Caching#
The cache lifetime depends on whether the URL follows the current revision or pins one:
| URL | Cache-Control | Meaning |
|---|---|---|
Stable link, no v | public, max-age=300, stale-while-revalidate=3600, stale-if-error=3600, no-transform | Caches may reuse a response for five minutes, then must check again. |
Pinned with ?v= | public, max-age=31536000, immutable, stale-while-revalidate=604800, stale-if-error=604800, no-transform | A revision's bytes never change, so caches keep it for a year. |
| Private file | private, no-store, max-age=0 | Never stored by shared caches, so revoking access takes effect on the next request. |
What this means after a replacement or rollback: SteadyLink itself switches to the new current revision immediately, but browsers and caches that already hold the stable link may serve the previous bytes until their five minutes are up. stale-while-revalidate allows a cache to answer one more request with the old copy while it fetches the new one. Plan on up to about five minutes before every viewer sees a change.
When a page must show a specific revision the moment it ships, link to the pinned URL (?v=4) in that release. When a page should always show the latest file, use the stable link and accept the short delay.
Signed links#
A signed link is a stable link with a token parameter that opens a private file until it expires or you revoke it. Each one is backed by a grant record you can list and revoke, so you can share a file with one customer, see which links exist, and cut access off without touching the file.
Create a signed link#
/api/assets/{asset_id}/signed-urlassets:writeQuery parameters
ttlintegerDefault300- Lifetime in seconds. Values are clamped to the range 60 (one minute) to 2,592,000 (30 days).
namestring- Internal label shown when listing grants, such as
Acme review. Trimmed and cut to 200 characters. revisioninteger- Limit the grant to one revision number. Omit it to follow the current revision. An unknown revision returns
404 Revision not found.
curl -X POST "https://api.steadylink.io/api/assets/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/signed-url?ttl=86400&name=Acme%20review" \
-H "X-API-Key: $STEADYLINK_API_KEY"const signed = await steadylink.createSignedLink("3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71", {
ttlSeconds: 86400,
name: "Acme review",
});
console.log(signed.url); // https://cdn.steadylink.io/a/3f2a9c1e-...?token=v2....from urllib.parse import urlencode
asset_id = "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71"
grant = client.request("POST", f"/api/assets/{asset_id}/signed-url?{urlencode({'ttl': 86400, 'name': 'Acme review'})}")
url = f"https://cdn.steadylink.io/a/{asset_id}?{urlencode({'token': grant['token']})}"steadylink link 3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71 --signed --ttl 86400 --name "Acme review"{
"id": "1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97",
"token": "v2.3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71.1791472980.1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97.4b1e9f0c7a2d...",
"name": "Acme review",
"revisionNumber": null,
"expiresAt": "2026-10-09T15:23:00.118204"
}Append the token to the delivery URL as ?token=.... Image parameters can be combined with it, for example ?token=...&w=800&fm=webp.
Things to know:
- The token is returned once. Listing grants does not return tokens. Store the URL if you need to send it again, or create a new grant.
- Revision grants need
v. A grant created withrevision=3only opens?v=3&token=.... The same token withoutv, or with anotherv, returns403. The SDK'ssigned.urldoes not addvfor you; usesteadylink.link(assetId, { token: signed.token, version: 3 }). - Public files ignore tokens. A signed link to a public file works, but so does the plain link. Make the file private to restrict it. See Set file visibility.
- Bucket grants cover the bucket. A grant created on a bucket ID also opens every private file in that bucket. Create grants on file asset IDs unless you mean to share the whole bucket.
- Creating a grant is an expensive operation and counts toward the expensive-operation limits in Plan access.
List signed links#
/api/assets/{asset_id}/signed-urlsassets:readReturns the 200 most recent grants for a file, newest first, including expired and revoked ones. Tokens are never included.
curl "https://api.steadylink.io/api/assets/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/signed-urls" \
-H "X-API-Key: $STEADYLINK_API_KEY"const { items } = await steadylink.listSignedLinks("3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71");
const active = items.filter((grant) => !grant.revokedAt && new Date(grant.expiresAt + "Z") > new Date());grants = client.request("GET", "/api/assets/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/signed-urls")["items"]{
"items": [
{
"id": "1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97",
"name": "Acme review",
"revisionNumber": null,
"expiresAt": "2026-10-09T15:23:00.118204",
"revokedAt": null,
"createdAt": "2026-10-08T15:23:00.118204"
}
]
}A grant is active when revokedAt is null and expiresAt is in the future. Timestamps are UTC without an offset.
Revoke a signed link#
/api/assets/{asset_id}/signed-urls/{grant_id}assets:writeStops a signed link before it expires. Private responses are never cached, so the next request with that token returns 403.
curl -X DELETE "https://api.steadylink.io/api/assets/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/signed-urls/1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97" \
-H "X-API-Key: $STEADYLINK_API_KEY"await steadylink.revokeSignedLink("3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71", "1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97");client.request("DELETE", "/api/assets/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/signed-urls/1c8e5a3f-2d7b-4a9c-b6e1-8f0d2c4a6b97"){ "revoked": true }A grant ID that does not belong to this file returns 404 Signed access grant not found. Revoking an already revoked grant succeeds again and updates revokedAt.
Other delivery tools#
Get a preview URL#
/api/assets/object/{asset_id}/previewassets:readReturns a short-lived URL to view a file without going through public delivery. Previews work for private files without a grant and are not counted in delivery analytics, which makes them right for admin tools and review screens.
Query parameters
vinteger- Revision to preview. Omit it for the current revision.
thumbnailbooleanDefaultfalse- Return a 480-pixel-wide WebP thumbnail instead of the original. Images only.
curl "https://api.steadylink.io/api/assets/object/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/preview?thumbnail=true" \
-H "X-API-Key: $STEADYLINK_API_KEY"{ "url": "https://storage-endpoint.example/transforms/e4d7b1a9-.../3b9c....webp?X-Amz-Expires=300&X-Amz-Signature=..." }The URL expires after five minutes; request a new one each time you render. A revision that is not yet clean returns 409 Revision is not ready for preview. thumbnail=true on a non-image returns 415, and on an image larger than 64 MiB returns 413.
Purge cached image variants#
/api/assets/{asset_id}/purgeassets:writeForgets every stored image variant of a file, across all its revisions. The next request for each size or format renders it again from the original. You rarely need this, because a new revision always gets fresh variants. It does not clear browser or edge caches.
curl -X POST "https://api.steadylink.io/api/assets/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/purge" \
-H "X-API-Key: $STEADYLINK_API_KEY"{ "purged": true }Purging counts as an expensive operation.
Next steps#
When to use private files and signed links, and how to share them safely.
Responsive images, art direction, and focal points in practice.
Serve stable links through next/image with the SteadyLink loader.
Current and pinned revisions, and what changes when you replace a file.