Files and folders
List, search, and inspect files, create and move folders, rename files, change visibility, replace a file's bytes without changing its link, and delete files.
On this page
- How files are identified
- Endpoints
- Browse and inspect
- List a folder
- List every file in the workspace
- Get a file by asset ID
- Get a file by key
- Search
- Folders
- Create a folder
- Rename or move a folder
- Delete a folder
- Change a file
- Rename or move a file
- Set file visibility
- Replace a file
- Create a replacement upload
- Finish a replacement
- Delete
- Delete a file
- Next steps
Use these endpoints to work with what is inside a bucket: browse folders, find a file and its stable link, move it, make it public or private, publish new bytes under the same link, or delete it. To add new files, use Uploads.
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"]).
How files are identified#
A file has three identifiers. Mixing them up is the most common source of 404 responses.
| Identifier | Example | What it is | Used by |
|---|---|---|---|
| Asset ID | 3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71 | The file's permanent identity. It appears in the delivery URL https://cdn.steadylink.io/a/{asset_id} and never changes, even when the file is renamed, moved, or replaced. Listings call it objectAssetId. | Revisions, signed links, delivery, Get a file by asset ID |
| Key | campaign/launch/hero.webp | The file's path inside its bucket. It changes when you rename or move the file. | Rename, visibility, replace, delete, Get a file by key |
| Listing ID | a91d3c70-... | The id of a row in a folder listing. Internal bookkeeping. | Rarely needed |
Keys are bucket-relative, have no leading slash, and use / between folders. Send them URL-encoded in query strings.
Endpoints#
| Operation | Method and path | Scope |
|---|---|---|
| List a folder | GET /api/assets/{bucket_id}/objects | assets:read |
| List every file in the workspace | GET /api/assets/objects | assets:read |
| Get a file by asset ID | GET /api/assets/object/{asset_id}/stat | assets:read |
| Get a file by key | GET /api/assets/{bucket_id}/objects/stat | assets:read |
| Search | GET /api/assets/_search | assets:read |
| Create a folder | POST /api/assets/{bucket_id}/folders | assets:write |
| Rename or move a folder | POST /api/assets/{bucket_id}/folders/rename | assets:write |
| Delete a folder | DELETE /api/assets/{bucket_id}/folders | assets:write |
| Rename or move a file | POST /api/assets/{bucket_id}/objects/rename | assets:write |
| Set file visibility | POST /api/assets/{bucket_id}/objects/visibility | assets:write |
| Create a replacement upload | POST /api/assets/{bucket_id}/objects/upload-temp | assets:write |
| Finish a replacement | POST /api/assets/{bucket_id}/objects/replace | assets:write |
| Delete a file | DELETE /api/assets/{bucket_id}/objects | assets:write |
Browse and inspect#
List a folder#
/api/assets/{bucket_id}/objectsassets:readReturns the folders and files directly inside one folder, not its subfolders' contents. Files are sorted by name. There is no pagination: the whole folder is returned at once.
Query parameters
prefixstring- Folder path, for example
campaign/launch/. A trailing slash is added if missing and a leading slash is removed. Omit it for the bucket root. rev_onlybooleanDefaultfalse- Return only
prefixandrevision. Use it to poll cheaply for changes.
curl "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects?prefix=campaign/" \
-H "X-API-Key: $STEADYLINK_API_KEY"const listing = await steadylink.listFiles("7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", { prefix: "campaign" });
for (const file of listing.items) console.log(file.key, steadylink.link(file.objectAssetId!));listing = client.request("GET", "/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects?prefix=campaign/")steadylink ls marketing:campaign/{
"prefix": "campaign/",
"folders": [
{ "name": "launch", "path": "campaign/launch/" }
],
"items": [
{
"id": "a91d3c70-4e2b-4f18-9c6d-7b0e5a2f8c13",
"objectAssetId": "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71",
"key": "campaign/hero.webp",
"name": "hero.webp",
"size": 2841024,
"lastModified": "2026-10-08T14:40:12.551930",
"visibility": "public",
"contentType": "image/webp"
}
],
"revision": "5c0f2b8e9a7d41c3b6e0f9a2d8c7b1e4f3a6d509"
}Response fields
items[].objectAssetIduuid | null- The file's asset ID, used in its stable link.
items[].visibilitystringpublicorprivate. When the file has no explicit visibility, the bucket's privacy is reported.items[].sizeinteger- Size of the current revision in bytes.
revisionstring | null- A hash of the folder's contents. It changes when a file in this folder is added, replaced, renamed, or removed, or a subfolder appears or disappears.
Files come straight from the database, so new and changed files appear immediately. Folder entries that exist only in storage are cached for up to three seconds. Compare revision values to decide whether to re-render a file browser.
List every file in the workspace#
/api/assets/objectsassets:readReturns every file in every bucket of the workspace as a flat list, without walking folders. Use it for inventories, audits, and sync jobs.
Query parameters
limitintegerDefault1000- Files per page, 1 to 1000. Values outside the range return
400. cursoruuid- The
nextCursorfrom the previous page. An invalid value returns422 Invalid cursor. includeFoldersbooleanDefaultfalse- Also return every folder path in every bucket. This scans storage and is slower on large workspaces; request it once rather than on every page.
curl "https://api.steadylink.io/api/assets/objects?limit=500" \
-H "X-API-Key: $STEADYLINK_API_KEY"let cursor: string | undefined;
do {
const page = await steadylink.listAllFiles({ limit: 500, cursor });
for (const file of page.items) console.log(file.bucketName, file.key);
cursor = page.nextCursor ?? undefined;
} while (cursor);cursor = None
while True:
query = "limit=500" + (f"&cursor={cursor}" if cursor else "")
page = client.request("GET", f"/api/assets/objects?{query}")
for item in page["items"]:
print(item["bucketName"], item["key"])
cursor = page["nextCursor"]
if not cursor:
break{
"items": [
{
"id": "a91d3c70-4e2b-4f18-9c6d-7b0e5a2f8c13",
"objectAssetId": "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71",
"bucketId": "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15",
"bucketName": "Marketing",
"key": "campaign/hero.webp",
"name": "hero.webp",
"parent": "campaign/",
"size": 2841024,
"lastModified": "2026-10-08T14:40:12.551930",
"visibility": "public",
"contentType": "image/webp"
}
],
"nextCursor": null,
"folders": []
}nextCursor is null on the last page. Items are ordered by listing ID, not by name or date.
Get a file by asset ID#
/api/assets/object/{asset_id}/statassets:readReturns a file's bucket, key, and details of its current revision, or of one revision with v. This is the most reliable way to go from a stable link back to the file.
Query parameters
vinteger- Revision number to describe. Omit it for the current revision.
curl "https://api.steadylink.io/api/assets/object/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/stat" \
-H "X-API-Key: $STEADYLINK_API_KEY"const file = await steadylink.getFile("3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71");
console.log(file.bucketId, file.key, file.scanStatus);file = client.request("GET", "/api/assets/object/3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71/stat")steadylink inspect 3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71{
"bucketId": "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15",
"key": "campaign/hero.webp",
"id": null,
"objectAssetId": "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71",
"size": 2841024,
"contentType": "image/webp",
"etag": "9b2e6f0c4a7d18e3b5f2c9a0d6e4b7f1a3c8e2d5f0b9a6c4e7d1f3b8a2c5e9d0",
"lastModified": "2026-10-08T14:40:12.551930",
"metadata": {
"image": { "width": 2400, "height": 1600, "format": "WEBP", "mode": "RGB", "animated": false, "frames": 1 }
},
"metadataStatus": "complete",
"assetVersionId": "e4d7b1a9-2c6f-4083-9a5e-1b8d3f0c6e27",
"scanStatus": "clean",
"scanEngine": "clamav",
"scanAt": "2026-10-08T14:40:15.002461",
"scanDetails": null
}Response fields
etagstring- SHA-256 of the revision's bytes. The same value is the delivery
ETag. metadataobject- Metadata extracted after upload. For raster images it has an
imageobject (width, height, format, color mode, animation) and, when present, up to 100exiftags. Other file types get an empty object. metadataStatusstringpendingorprocessinguntil extraction has run, thencomplete, orerrorif the file could not be read.scanStatusstring | nullpending,clean,infected, orerror.nullwhen scanning is off. Onlycleanrevisions are delivered.assetVersionIduuid- ID of the revision described.
A v that does not exist, or an ID that is not a file, returns 404 Not found.
Get a file by key#
/api/assets/{bucket_id}/objects/statassets:readLooks a file up by its path and returns its asset ID, size, and storage metadata.
Query parameters
keystringRequired- Bucket-relative key, for example
campaign/hero.webp. exifbooleanDefaultfalse- For images, add EXIF tags and
image_width,image_height, andimage_formattometadata.
curl "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/stat?key=campaign%2Fhero.webp" \
-H "X-API-Key: $STEADYLINK_API_KEY"const file = await steadylink.statFile("7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", "campaign/hero.webp");from urllib.parse import quote
file = client.request("GET", f"/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/stat?key={quote('campaign/hero.webp', safe='')}"){
"key": "campaign/hero.webp",
"id": "a91d3c70-4e2b-4f18-9c6d-7b0e5a2f8c13",
"objectAssetId": "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71",
"size": 0,
"contentType": "application/octet-stream",
"etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
"lastModified": "2026-10-08T14:52:03+00:00",
"metadata": { "asset-id": "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71" }
}This route reads the file's entry in bucket storage, not its revision. For files whose bytes live in revisions, the entry is a small marker, so size, contentType, and etag describe the marker rather than the file. Rely on objectAssetId from this response, then call Get a file by asset ID for the real size, type, and scan state. When no storage entry exists for the key, the route returns 404 Not found even if the file appears in listings; in that case find the asset ID from List a folder.
Search#
/api/assets/_searchassets:readFinds files by name or path, buckets by name, PDFs by their extracted text, and recent workspace activity.
Query parameters
qstringRequired- Search text. Matching is case-insensitive and finds the text anywhere in the name or path. An empty value returns no results.
scopestringDefaultorgorgsearches the whole workspace.bucketsearches one bucket and skips bucket names and activity.bucket_iduuid- Required when
scope=bucket. Missing returns400. limitintegerDefault20- Maximum results, clamped to 1 to 50.
curl "https://api.steadylink.io/api/assets/_search?q=hero&limit=10" \
-H "X-API-Key: $STEADYLINK_API_KEY"{
"query": "hero",
"scope": "org",
"items": [
{
"type": "object",
"assetId": "3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71",
"bucketId": "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15",
"name": "hero.webp",
"path": "campaign/hero.webp",
"visibility": "public"
},
{
"type": "content",
"assetId": "8d0b4e2f-6a19-4c73-b5e8-0f2d7a9c1b46",
"bucketId": "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15",
"name": "brand-guide.pdf",
"path": "guides/brand-guide.pdf",
"version": 3,
"snippet": "...place the hero image above the fold on every landing page..."
}
]
}Results have a type: object (a file whose name or path matches), bucket, content (text inside a PDF, with the revision version and a snippet), or event (an activity entry with createdAt). Exact name matches come first, then prefix matches, then other matches, then content and events.
Folders#
Folders are paths. A file's folder is the part of its key before the last /, and a folder exists as long as a file uses it or it was created explicitly. You do not need to create a folder before uploading into it: an upload with path: "campaign/launch/" creates the path.
Create a folder#
/api/assets/{bucket_id}/foldersassets:writeCreates an empty folder so it shows up in listings before any file is added.
Query parameters
pathstringRequired- Folder path, for example
campaign/launch. Leading slashes are removed and a trailing slash is added.
curl -X POST "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/folders?path=campaign/launch" \
-H "X-API-Key: $STEADYLINK_API_KEY"await steadylink.createFolder("7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", "campaign/launch");client.request("POST", "/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/folders?path=campaign/launch"){ "created": true }Rename or move a folder#
/api/assets/{bucket_id}/folders/renameassets:writeMoves a folder and everything under it to a new path. The files keep their asset IDs and stable links; only their keys change. POST /api/assets/{bucket_id}/folders/move is an alias with the same parameters.
Query parameters
from_pathstringRequired- Current folder path, for example
campaign/launch. to_pathstringRequired- New folder path, for example
archive/2026/launch. overwritebooleanDefaultfalse- Accepted for compatibility. Do not rely on the route to protect an existing destination: check that
to_pathis empty or new before you move.
curl -X POST "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/folders/rename?from_path=campaign/launch&to_path=archive/2026/launch" \
-H "X-API-Key: $STEADYLINK_API_KEY"{ "renamed": true, "moved": 1 }moved counts storage entries that were copied, which can differ from the number of files. Moving a folder into its own subtree, such as campaign into campaign/old, returns 400.
Delete a folder#
/api/assets/{bucket_id}/foldersassets:writeQuery parameters
pathstringRequired- Folder path to remove, for example
campaign/old.
curl -X DELETE "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/folders?path=campaign/old" \
-H "X-API-Key: $STEADYLINK_API_KEY"{ "deleted": true, "objectsDeleted": 4 }Removes the folder and every file listing under it, including subfolders. objectsDeleted counts storage entries removed, not files. It does not delete the files' revisions, so their storage is not released. When you want files gone completely, delete each file first, then the folder.
Change a file#
Rename or move a file#
/api/assets/{bucket_id}/objects/renameassets:writeChanges a file's key. Use a key in another folder to move it. The asset ID, stable link, and revisions do not change, so nothing that links to the file breaks.
Query parameters
from_keystringRequired- Current key, for example
campaign/hero.webp. to_keystringRequired- New key, for example
campaign/launch/hero.webp. If a file already uses it, the request returns409 Destination already exists.
curl -X POST "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/rename?from_key=campaign%2Fhero.webp&to_key=campaign%2Flaunch%2Fhero.webp" \
-H "X-API-Key: $STEADYLINK_API_KEY"await steadylink.renameFile("7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", "campaign/hero.webp", "campaign/launch/hero.webp");from urllib.parse import urlencode
query = urlencode({"from_key": "campaign/hero.webp", "to_key": "campaign/launch/hero.webp"})
client.request("POST", f"/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/rename?{query}"){ "renamed": true }The route answers {"renamed": true} even when no file has from_key. If you are not sure the key exists, check it in a listing first.
Set file visibility#
/api/assets/{bucket_id}/objects/visibilityassets:writeMakes one file public, private, or dependent on its bucket.
Query parameters
keystringRequired- Bucket-relative key.
visibilitystringRequiredpublicserves the file to anyone with the link.privaterequires a signed link.inheritfollows the bucket's privacy. Anything else returns400.
curl -X POST "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/visibility?key=campaign%2Fhero.webp&visibility=private" \
-H "X-API-Key: $STEADYLINK_API_KEY"await steadylink.setVisibility("7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", "campaign/hero.webp", "private");client.request("POST", "/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/visibility?key=campaign%2Fhero.webp&visibility=private"){ "updated": true }Public responses may already be cached by browsers and shared caches for up to five minutes. Making a file private stops new public responses right away but cannot recall copies that were already cached. See Caching.
Replace a file#
Replacing a file publishes new bytes as the next revision of the same asset. The asset ID and every link to it stay the same, and the previous revisions are kept so you can roll back. This takes three requests: reserve a temporary upload, send the bytes, and finish.
import { fileFromPath } from "@steadylink/sdk/node";
// By asset ID...
const result = await steadylink.replace(
"3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71",
await fileFromPath("./hero-v2.webp"),
);
// ...or by bucket and key
await steadylink.replace(
{ bucketId: "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", key: "campaign/hero.webp" },
await fileFromPath("./hero-v2.webp"),
);
console.log(result.version, result.url); // new revision number, same linkfrom pathlib import Path
bucket_id = "7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15"
data = Path("hero-v2.webp").read_bytes()
pending = client.create_replacement_upload(bucket_id, len(data), "image/webp")
client.upload_to_url(pending["uploadUrl"], data) # signed for application/octet-stream
result = client.finish_replacement(bucket_id, "campaign/hero.webp", pending["tempKey"], "hero-v2.webp")
print(result["version"])steadylink replace 3f2a9c1e-8b4d-4c7a-a1e2-6d5f0b9c3e71 ./hero-v2.webp
steadylink replace marketing:campaign/hero.webp ./hero-v2.webpCreate a replacement upload#
/api/assets/{bucket_id}/objects/upload-tempassets:writeReturns a presigned URL for the new bytes and a temporary key that identifies them.
Query parameters
sizeintegerRequired- Exact size of the new file in bytes. Larger than the plan's upload limit returns
413with the codeupload_size_limit. content_typestringDefaultapplication/octet-stream- The file's type, recorded for your audit trail. The stored type is detected from the bytes when you finish.
curl -X POST "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/upload-temp?size=2950112&content_type=image%2Fwebp" \
-H "X-API-Key: $STEADYLINK_API_KEY"{
"uploadUrl": "https://storage-endpoint.example/uploads/temp/52e8a1c7-...?X-Amz-Signature=...",
"tempKey": "uploads/temp/52e8a1c7-9d3b-4f60-8e2a-b4c1d7f09a35"
}Then send the bytes to uploadUrl with a PUT and Content-Type: application/octet-stream. The URL is signed for that content type, so any other value fails with 403 from storage. It expires after 15 minutes. Do not send your API key to it.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary @hero-v2.webpFinish a replacement#
/api/assets/{bucket_id}/objects/replaceassets:writeTurns the uploaded bytes into the file's next revision and makes it current.
Query parameters
keystringRequired- Key of the file to replace, for example
campaign/hero.webp. upload_temp_keystringRequired- The
tempKeyfrom Create a replacement upload. If no bytes were uploaded to it, the request returns400 upload_temp_key not found. original_filenamestring- Filename of the new bytes, for example
hero-v2.webp. It is used as the revision's stored filename, which delivery uses inContent-Disposition. The key does not change.
curl -X POST "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects/replace?key=campaign%2Fhero.webp&upload_temp_key=uploads%2Ftemp%2F52e8a1c7-9d3b-4f60-8e2a-b4c1d7f09a35&original_filename=hero-v2.webp" \
-H "X-API-Key: $STEADYLINK_API_KEY"{ "replaced": true, "version": 4 }What to expect:
- The new revision becomes current immediately. While its malware scan is running, the stable link answers
423 Scanning. Once the scan finishes clean, the link serves the new bytes. If the scan finds malware, the link answers403until you promote an earlier clean revision. - Caches catch up within minutes. Browsers and shared caches can keep the previous bytes for up to five minutes. Links pinned with
?v=never change. See Caching. - Storage is reserved before the bytes are stored. If the new revision would push the workspace over its storage limit, the request returns
413. Every revision you keep counts toward storage. - A key with no file creates one. If nothing exists at
key, the request creates a new file there with a new asset ID. Checkversion:1means a new file was created.
Delete#
Delete a file#
/api/assets/{bucket_id}/objectsassets:writeDeletes a file, every revision, and every cached image variant, and releases their storage. The stable link stops working and returns 404.
Query parameters
keystringRequired- Bucket-relative key, for example
campaign/old-hero.webp.
curl -X DELETE "https://api.steadylink.io/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects?key=campaign%2Fold-hero.webp" \
-H "X-API-Key: $STEADYLINK_API_KEY"await steadylink.deleteFile("7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15", "campaign/old-hero.webp");client.request("DELETE", "/api/assets/7c1e4b2a-5d3f-4e8a-9b61-2f0c8d4a7e15/objects?key=campaign%2Fold-hero.webp")steadylink rm marketing:campaign/old-hero.webp --yes{ "deleted": true }To delete only an old revision and keep the file, use Delete a revision.