Skip to content

Upload lifecycle

The states an upload passes through, from upload session to scanned revision, and every check that can stop a file along the way.

On this page

An upload is not finished when the bytes arrive. SteadyLink checks the size, the real file type, your bucket's rules, and the file's contents for malware before the file becomes a revision anyone can download. This page walks through each stage so you can build reliable upload code, read upload status correctly, and explain to a user why a file did not go through.

Createupload batchUploadPUT the bytesCompletefinalize sessionScanmalware + policyreadyblockedfailed
Upload sessions start as created, move through committing and scanning, and end ready, blocked, or failed. A session can also be cancelled before it finishes.

The three steps#

Every API upload follows the same three steps. The dashboard, the TypeScript SDK's upload(), and the CLI run them for you.

  1. Create an upload batch

    POST /api/upload-batches with the destination bucket and up to 100 files. For each file you send its name, an optional folder path, and its exact size in bytes. SteadyLink checks each file against your plan's size limit, reserves storage for it, and returns one upload session per file with a presigned uploadUrl.

  2. Send the bytes

    PUT the raw file to the session's uploadUrl with Content-Type: application/octet-stream. This request goes straight to object storage, not to the SteadyLink API, so it does not use your API key and does not count as an API operation.

  3. Complete the session

    POST /api/upload-sessions/{session_id}/complete. SteadyLink answers 202 Accepted right away and finalizes the file in the background. Poll GET /api/upload-batches/{batch_id} or listen on GET /api/events/stream until the session reaches a terminal state.

The Uploads API reference has full request and response examples. The browser uploads guide shows how to run the second step from a user's browser without exposing your key.

Session states#

statusMeaningTerminal
createdThe session exists and is waiting for bytes and completion.No
uploadedBytes were streamed through the API instead of the presigned URL.No
committingCompletion was requested. SteadyLink is checking and storing the file.No
scanningThe file is stored as a revision and the malware scan is running.No
readyThe file passed every check and is available under its visibility settings.Yes
blockedA bucket policy or the malware scanner rejected the file. Nothing is delivered.Yes
failedFinalization could not complete. The error object says why.Yes
cancelledThe session was cancelled before it finished.Yes

A batch's own status is active while any session is still moving, then complete if every file is ready, or partial if any file ended blocked or failed.

What is checked, and where#

Checks run in this order. The first one that fails stops the file.

When the batch is created#

  • Size is required. A file without size is rejected with 422 upload_size_required. SteadyLink needs the size up front to enforce limits and reserve storage.
  • Plan file size limit. A file larger than your plan allows is rejected with 413 upload_size_limit. Uploads made with an API key are also held to the API plan's upload limit, whichever is lower. See Usage and limits.
  • Storage. Each file's size is reserved against your storage allowance while the session is open. If the workspace would go over, the batch is refused. Reservations are released when a session is cancelled, expires, fails, or is blocked.
  • Batch size. More than 100 files in one batch is rejected with 422. Split larger sets into several batches.

During finalization#

  • The bytes arrived. If nothing was uploaded to the presigned URL, or the temporary bytes expired, the session ends failed with upload_missing.
  • Exact size. The stored bytes must match the declared size. A different size ends failed with upload_size_mismatch; a file over the limit ends failed with upload_too_large.
  • Real file type. SteadyLink reads the bytes to detect what the file really is, and stores and serves the detected type. When a request declares a specific type that belongs to a different family than the detected one (for example a "PNG" that is really a PDF), the file is rejected with 415 MIME mismatch. A bucket can turn this check off with its block_mime_mismatch setting.
  • Bucket policy. A bucket can set a maximum upload size in MB (max_upload_mb), allowed MIME type prefixes such as image/ (allowed_mime_prefixes), and allowed filename extensions such as pdf (allowed_extensions). The type prefix is compared with the detected type, not the name. A file that breaks any of these ends blocked with the error code policy and a message such as File type not allowed by bucket policy. See Buckets and folders.

After the revision is stored#

  • Malware scan. Every revision is scanned with ClamAV. A clean result moves the session to ready. A detection moves it to blocked with malware_detected, and the infected revision can never be delivered or restored.

If the file was a replacement, the new revision becomes current as soon as it is stored, before the scan finishes. While the scan runs, the file's stable link answers 423 Locked rather than serve unscanned bytes. See The scan window after a replacement.

Time limits#

LimitValueWhat happens when it passes
Presigned uploadUrl15 minutes from batch creationObject storage refuses the PUT. Create a new batch.
Upload session (expiresAt)Shown on each session; 24 hours by defaultCompleting returns 410 Upload session expired, the session ends failed with upload_expired, and its storage reservation is released.

Start the PUT promptly after creating the batch. The 15-minute window covers starting the request; for large files on slow connections, create the batch just before you upload rather than queuing many batches ahead of time.

Completing safely more than once#

Completion is designed to be retried. Send an Idempotency-Key header with a value that is stable for the session, such as upload- followed by the session ID:

Complete an upload
curl -X POST "https://api.steadylink.io/api/upload-sessions/$SESSION_ID/complete" \
  -H "X-API-Key: $STEADYLINK_API_KEY" \
  -H "Idempotency-Key: upload-$SESSION_ID"
  • If the session already reached a terminal state, the call returns that state again instead of doing anything.
  • If the session is still committing, the call returns 409 Upload is already committing. Treat this as "in progress" and poll the batch.
  • If the session was completed earlier with a different idempotency key, the call returns 409 Idempotency key mismatch.

So a client that times out can always repeat the same completion call. It never creates a second revision.

If background finalization hits a temporary problem, SteadyLink retries it on its own, with the session showing committing and the error code commit_retrying. After repeated failures the session ends failed with commit_failed and its storage reservation is released.

Cancelling#

DELETE /api/upload-sessions/{session_id} cancels a session that has not finished and releases its storage reservation. Sessions that already ended ready, blocked, or failed cannot be cancelled and return 409.

Next steps#