Conversions API
Convert images, documents, spreadsheets, audio, video, archives, and ebooks, then download the result or save it to a bucket.
On this page
- How a conversion job works
- Access: anonymous, session, or API key
- Catalog and limits
- Get the catalog
- Jobs
- Create a conversion
- Start a conversion
- Upload through the API
- Get a conversion
- Download the result
- Download several results as an archive
- Save the result to a bucket
- Cancel or delete a conversion
- List recent conversions
- List convertible workspace files
- Next steps
Use these routes to convert a file you upload, or a file already stored in a workspace, into another format. A conversion is a temporary job: you create it, send the source bytes, start it, poll until it completes, and then download the result or save it into a bucket as a normal file. For a dashboard walkthrough, see File conversions.
How a conversion job works#
Create the job
POST /api/conversionschecks the format pair, options, size, and your limits, then returns the job with a presigneduploadUrl. If you convert a stored file instead, there is nothing to upload and the job is queued immediately.Upload the source
PUTthe exact bytes touploadUrlwithContent-Type: application/octet-stream. The URL only accepts the size you declared.Start it
POST /api/conversions/{job_id}/startconfirms the upload and queues the job.Poll and collect
GET /api/conversions/{job_id}untilstatusiscompleteorfailed, then call/downloador/save.
A job moves through created, uploaded, queued, processing, and then one of complete, failed, cancelled, or expired. progress (0 to 100), stage, and stageDetail describe where it is.
Jobs and their files are deleted when expiresAt passes. Any request for an expired job returns 410. Download or save the result before then.
Access: anonymous, session, or API key#
The same routes serve three kinds of caller, and the job remembers which one created it.
| Caller | How it authenticates | Who can read the job later |
|---|---|---|
| Anonymous | No credentials | Anyone holding the job's accessToken, sent as X-Conversion-Token |
| Signed-in user | Authorization: Bearer (and X-Workspace-Id if they belong to several workspaces) | That user |
| API key | X-API-Key | That exact key. Another key in the same workspace gets 403. |
- Anonymous jobs get an
accessTokenin the create response, and only there. Store it with the job ID; without it the job cannot be read, started, or downloaded. - API keys need
assets:writeto create, start, upload, save, archive, or cancel jobs, andassets:readto read a job, download it, or list recent jobs. Authenticated jobs never return anaccessToken. - Signed-in viewers cannot create conversions, because conversions use workspace credits (
403,"permission": "conversions:run"). Saving a result needs a role that can write files.
Authenticated jobs count against the workspace's conversion allowance and run in a faster queue than anonymous jobs. serviceTier on the job shows which queue it uses: anonymous, member, or priority.
Catalog and limits#
Get the catalog#
/api/conversions/catalogReturns every supported format group and the limits for anonymous callers and each plan. Read it at startup instead of hardcoding formats or sizes; limits can change without a code release.
curl https://api.steadylink.io/api/conversions/catalog{
"groups": [
{
"id": "image",
"label": "Images",
"engine": "image",
"inputs": ["jpg", "jpeg", "png", "webp", "avif", "gif", "tiff", "bmp", "heic", "ico", "svg"],
"outputs": ["jpg", "png", "webp", "avif", "gif", "tiff", "bmp", "ico"],
"weight": 1
}
],
"retentionHours": 1,
"anonymous": {
"plan": "anonymous",
"concurrency": 2,
"maxBytes": 26214400,
"videoMaxBytes": 15728640,
"monthlyUnits": null,
"dailyJobs": 10,
"retentionHours": 1,
"videoMaxDurationSeconds": 120,
"videoMaxWidth": 1920,
"videoMaxHeight": 1080,
"videoConcurrency": 1,
"videoMonthlyUnits": null
},
"registered": { "plan": "hobby", "maxBytes": 104857600, "priorityQueue": false, "...": "same fields" },
"plans": {
"hobby": { "maxBytes": 104857600, "retentionHours": 24, "priorityQueue": false, "...": "same fields" },
"pro": { "maxBytes": 524288000, "retentionHours": 48, "priorityQueue": true, "...": "same fields" }
}
}The example is shortened. Supported groups:
| Group | Inputs | Outputs |
|---|---|---|
| Images | jpg, png, webp, avif, gif, tiff, bmp, heic, ico, svg | jpg, png, webp, avif, gif, tiff, bmp, ico |
| Documents | doc, docx, odt, rtf, txt, html | pdf, docx, odt, rtf, txt, html |
| Spreadsheets | xls, xlsx, ods, csv | pdf, xlsx, ods, csv |
| Presentations | ppt, pptx, odp | pdf, pptx, odp |
| jpg, png, txt, docx, pdf | ||
| Audio | mp3, wav, aac, flac, ogg, m4a, opus | the same seven |
| Video | mp4, webm, mov, avi, mkv | mp4, webm, mov, avi, mkv, gif, mp3 |
| Archives | zip, tar, tar.gz, 7z | zip, tar, tar.gz, 7z |
| Ebooks | epub, mobi, azw3, pdf, txt | epub, mobi, azw3, pdf, txt |
Format names are case-insensitive and may start with a dot. jpeg is read as jpg, tif as tiff, and tgz as tar.gz. Conversion is only possible within a group; a pair that no group supports returns 422.
Default limits by caller (the catalog is authoritative):
| Caller | Max file | Max video | Jobs at once | Kept for |
|---|---|---|---|---|
| Anonymous | 25 MB | 15 MB | 2, and 10 jobs per 24 hours | 1 hour |
| Hobby | 100 MB | 50 MB | 2 | 24 hours |
| Personal | 250 MB | 100 MB | 2 | 24 hours |
| Pro | 500 MB | 250 MB | 4 | 48 hours |
| Business | 1 GB | 500 MB | 6 | 72 hours |
Jobs#
Create a conversion#
/api/conversionsassets:writeSend either filename and size for a new upload, or sourceAssetId to convert a stored file. The scope applies when you call with an API key; anonymous callers send no credentials.
Body
inputFormatstringRequired- Source format, for example
png. outputFormatstringRequired- Target format, for example
avif. filenamestring- Source file name, up to 512 characters. Required unless
sourceAssetIdis set. sizeinteger- Exact source size in bytes. Required unless
sourceAssetIdis set. The upload must match it. contentTypestring- Source MIME type, recorded with the job.
sourceAssetIduuid- Convert a file already in the workspace. Requires a session or API key.
sourceVersioninteger- Revision number of
sourceAssetIdto convert. Defaults to the current revision. optionsobject- Conversion options. See below. Unknown keys return
422.
Options
widthinteger- Target width, 1 to 16384. Out-of-range values return
422. heightinteger- Target height, 1 to 16384.
qualityinteger- 1 to 100. Values outside the range are clamped.
fpsinteger- Frame rate for video and animated output, 1 to 30. Clamped.
backgroundstring- Background color value, up to 32 characters.
effortstring- Encoder effort:
fast,balanced, ormaximum.
Upload a new file:
curl -X POST https://api.steadylink.io/api/conversions \
-H "X-API-Key: $STEADYLINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "campaign.png",
"size": 2841024,
"inputFormat": "png",
"outputFormat": "avif",
"contentType": "image/png",
"options": { "quality": 82, "width": 1200 }
}'{
"id": "7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73",
"status": "created",
"category": "image",
"inputFormat": "png",
"outputFormat": "avif",
"sourceName": "campaign.png",
"sourceBytes": 2841024,
"outputName": null,
"outputBytes": null,
"outputMime": null,
"progress": 0,
"stage": "created",
"stageDetail": "Waiting for upload",
"serviceTier": "priority",
"timings": {},
"error": null,
"expiresAt": "2026-10-10T10:20:00.000000",
"savedAssetId": null,
"saveUploadSessionId": null,
"registered": true,
"createdAt": "2026-10-08T10:20:00.000000",
"uploadUrl": "<presigned PUT URL>"
}An anonymous caller gets the same response plus "accessToken" and "registered": false.
Then upload the bytes and start the job:
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary @campaign.pngConvert a stored file instead. The job starts as queued with no uploadUrl, so skip the upload and start steps:
curl -X POST https://api.steadylink.io/api/conversions \
-H "X-API-Key: $STEADYLINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceAssetId": "3f2a9c1e-6b7d-4e21-9a0c-1d5e8f7b2a44",
"inputFormat": "png",
"outputFormat": "avif",
"options": { "quality": 82, "width": 1200 }
}'A stored source must have passed the malware scan and must not be encrypted (409 otherwise), and inputFormat must match its file extension (422 otherwise).
Errors:
| Status | Code | Meaning |
|---|---|---|
413 | conversion_size_limit, video_size_limit | The source is larger than the caller's limit. |
422 | none | Unsupported format pair, invalid option, or missing filename and size. |
429 | conversion_daily_limit | Anonymous caller reached 10 jobs in 24 hours. |
429 | conversion_concurrency_limit, video_concurrency_limit | Too many jobs are active. Wait for one to finish. |
Start a conversion#
/api/conversions/{job_id}/startassets:writeChecks that the uploaded object exists and matches size, then queues the job. Calling it again on a job that is already queued, processing, or complete returns the job unchanged, so it is safe to retry.
curl -X POST https://api.steadylink.io/api/conversions/7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73/start \
-H "X-API-Key: $STEADYLINK_API_KEY"The response is the job with "status": "queued" and status 202 Accepted. Errors: 400 when the upload is missing or its size differs (the reserved credits are released), 409 when the job was cancelled or expired.
Upload through the API#
/api/conversions/{job_id}/uploadassets:writeA fallback for clients that cannot reach the storage URL, such as a browser blocked by a network policy. Stream the raw bytes as the request body; Content-Length, if sent, must equal size. Returns the job with "status": "uploaded". Then call start as usual. Prefer uploadUrl when you can, since it does not pass the bytes through the API.
Get a conversion#
/api/conversions/{job_id}assets:readReturns the job. Poll every one to two seconds while it is queued or processing.
curl https://api.steadylink.io/api/conversions/7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73 \
-H "X-Conversion-Token: $CONVERSION_TOKEN"{
"id": "7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73",
"status": "complete",
"progress": 100,
"stage": "complete",
"outputName": "campaign.avif",
"outputBytes": 184312,
"outputMime": "image/avif",
"error": null,
"expiresAt": "2026-10-08T11:20:00.000000",
"registered": false,
"...": "other job fields"
}When a job fails, error holds code and message. Errors: 403 without the right identity or token, 404 for an unknown ID, 410 once expired.
Download the result#
/api/conversions/{job_id}/downloadassets:readReturns a presigned URL for the converted file. It is JSON, not a redirect: fetch url within 15 minutes. Returns 409 until the job is complete.
curl https://api.steadylink.io/api/conversions/7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73/download \
-H "X-API-Key: $STEADYLINK_API_KEY"{
"url": "<presigned GET URL>",
"filename": "campaign.avif",
"expiresIn": 900
}Download several results as an archive#
/api/conversions/archiveassets:writeBundles 2 to 25 completed jobs into one zip or tar.gz file. Each item needs the same access as a single download, so anonymous items include their token. Duplicate output names are renamed (photo.avif, photo-2.avif). The combined output may be at most 1 GB (413 otherwise).
curl -X POST https://api.steadylink.io/api/conversions/archive \
-H "Content-Type: application/json" \
-d '{
"format": "zip",
"items": [
{ "id": "7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73", "token": "Jx3..." },
{ "id": "1c8f5a27-3d9e-4b06-a2f4-8e7b0d6c9a51", "token": "Pq9..." }
]
}'The response has the same shape as a single download, with filename set to steadylink-conversions.zip or steadylink-conversions.tar.gz. Returns 409 if any job is not complete.
Save the result to a bucket#
/api/conversions/{job_id}/saveassets:writeCopies the converted file into a bucket. It then goes through the same finalization and scanning as any upload and gets its own stable link. Requires a session or API key with a workspace.
Body
bucketIduuidRequired- Destination bucket in the caller's workspace.
pathstringDefault""- Folder inside the bucket, for example
converted/...segments are rejected. filenamestring- Name for the saved file. Defaults to
outputName.
curl -X POST https://api.steadylink.io/api/conversions/7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73/save \
-H "X-API-Key: $STEADYLINK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "bucketId": "9b1c7e52-0f3a-4c6d-8e2b-5a7d1c3e9f10", "path": "converted/", "filename": "campaign.avif" }'The response is the job with saveUploadSessionId set and status 202 Accepted. Saving happens in the background; once it finishes, savedAssetId holds the new file's asset ID. Saving twice returns the job without creating a second file.
An anonymous job can be claimed by someone who signs in afterward: call save with their session and the job's X-Conversion-Token. The job then belongs to them. A job that already belongs to another user or key returns 403.
Cancel or delete a conversion#
/api/conversions/{job_id}assets:writeCancels a job that has not finished and releases its reserved credits. For a finished job, it expires the job immediately so its files are removed. Both return { "cancelled": true }.
List recent conversions#
/api/conversions/recentassets:readReturns unexpired jobs that have not failed or been cancelled, newest first, for the calling user or key. limit defaults to 50, up to 100. Anonymous callers get 401.
List convertible workspace files#
/api/conversions/sourcesassets:readLists stored files that can be used as sourceAssetId: scanned clean and not encrypted. Supports q (matches file name, path, or bucket name), limit (1 to 50, default 25), and cursor (the nextCursor from the previous page). Each item includes assetId, name, size, contentType, version, bucketId, bucketName, path, and modifiedAt.