Skip to content

Conversions API

Convert images, documents, spreadsheets, audio, video, archives, and ebooks, then download the result or save it to a bucket.

On this page

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#

  1. Create the job

    POST /api/conversions checks the format pair, options, size, and your limits, then returns the job with a presigned uploadUrl. If you convert a stored file instead, there is nothing to upload and the job is queued immediately.

  2. Upload the source

    PUT the exact bytes to uploadUrl with Content-Type: application/octet-stream. The URL only accepts the size you declared.

  3. Start it

    POST /api/conversions/{job_id}/start confirms the upload and queues the job.

  4. Poll and collect

    GET /api/conversions/{job_id} until status is complete or failed, then call /download or /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.

CallerHow it authenticatesWho can read the job later
AnonymousNo credentialsAnyone holding the job's accessToken, sent as X-Conversion-Token
Signed-in userAuthorization: Bearer (and X-Workspace-Id if they belong to several workspaces)That user
API keyX-API-KeyThat exact key. Another key in the same workspace gets 403.
  • Anonymous jobs get an accessToken in 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:write to create, start, upload, save, archive, or cancel jobs, and assets:read to read a job, download it, or list recent jobs. Authenticated jobs never return an accessToken.
  • 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#

GET/api/conversions/catalog
No API key. Public route.

Returns 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
curl https://api.steadylink.io/api/conversions/catalog
200 OKResponse
{
  "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:

GroupInputsOutputs
Imagesjpg, png, webp, avif, gif, tiff, bmp, heic, ico, svgjpg, png, webp, avif, gif, tiff, bmp, ico
Documentsdoc, docx, odt, rtf, txt, htmlpdf, docx, odt, rtf, txt, html
Spreadsheetsxls, xlsx, ods, csvpdf, xlsx, ods, csv
Presentationsppt, pptx, odppdf, pptx, odp
PDFpdfjpg, png, txt, docx, pdf
Audiomp3, wav, aac, flac, ogg, m4a, opusthe same seven
Videomp4, webm, mov, avi, mkvmp4, webm, mov, avi, mkv, gif, mp3
Archiveszip, tar, tar.gz, 7zzip, tar, tar.gz, 7z
Ebooksepub, mobi, azw3, pdf, txtepub, 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):

CallerMax fileMax videoJobs at onceKept for
Anonymous25 MB15 MB2, and 10 jobs per 24 hours1 hour
Hobby100 MB50 MB224 hours
Personal250 MB100 MB224 hours
Pro500 MB250 MB448 hours
Business1 GB500 MB672 hours

Jobs#

Create a conversion#

POST/api/conversions
Requiresassets:write

Send 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 sourceAssetId is set.
sizeinteger
Exact source size in bytes. Required unless sourceAssetId is 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 sourceAssetId to 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, or maximum.

Upload a new file:

curl
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 }
  }'
201 CreatedResponse
{
  "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
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @campaign.png

Convert a stored file instead. The job starts as queued with no uploadUrl, so skip the upload and start steps:

curl
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:

StatusCodeMeaning
413conversion_size_limit, video_size_limitThe source is larger than the caller's limit.
422noneUnsupported format pair, invalid option, or missing filename and size.
429conversion_daily_limitAnonymous caller reached 10 jobs in 24 hours.
429conversion_concurrency_limit, video_concurrency_limitToo many jobs are active. Wait for one to finish.

Start a conversion#

POST/api/conversions/{job_id}/start
Requiresassets:write

Checks 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
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#

PUT/api/conversions/{job_id}/upload
Requiresassets:write

A 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#

GET/api/conversions/{job_id}
Requiresassets:read

Returns the job. Poll every one to two seconds while it is queued or processing.

curl
curl https://api.steadylink.io/api/conversions/7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73 \
  -H "X-Conversion-Token: $CONVERSION_TOKEN"
200 OKResponse
{
  "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#

GET/api/conversions/{job_id}/download
Requiresassets:read

Returns 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
curl https://api.steadylink.io/api/conversions/7d4e2b19-8a3c-4f50-b6e1-0c9a2d5f8e73/download \
  -H "X-API-Key: $STEADYLINK_API_KEY"
200 OKResponse
{
  "url": "<presigned GET URL>",
  "filename": "campaign.avif",
  "expiresIn": 900
}

Download several results as an archive#

POST/api/conversions/archive
Requiresassets:write

Bundles 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
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#

POST/api/conversions/{job_id}/save
Requiresassets:write

Copies 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
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#

DELETE/api/conversions/{job_id}
Requiresassets:write

Cancels 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#

GET/api/conversions/recent
Requiresassets:read

Returns 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#

GET/api/conversions/sources
Requiresassets:read

Lists 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.

Next steps#