Skip to content
Log inGet an API key
Concepts

Files

Audio never goes through the API itself. You ask for an upload, send the bytes straight to storage, finish the upload, and wait while we check the file.

How uploads work

  1. POST /v1/files with filename, size_bytes and content_type. You get a file id (status: uploading) and upload instructions in upload.
  2. Send the bytes to the signed URL(s) in upload: one PUT for files up to 100 MB (104,857,600 bytes), parts above that.
  3. POST /v1/files/{id}/complete. It answers 202 with status: validating; for multipart uploads, send the parts and their ETags.
  4. Poll GET /v1/files/{id} until status is ready (with duration_seconds) or rejected (with error).
File statuses
StatusMeaning
uploadingWaiting for the bytes and /complete. The upload URLs are valid for 6 hours.
validatingWe copy the upload and measure the audio. Usually a few seconds.
readyUsable in jobs. duration_seconds is set. Deleted 3 days later (expires_at).
rejectedNot audio we can decode (invalid_audio) or over 6 hours (audio_too_long). See error.
deletedRemoved by you or by retention.

Single upload (up to 100 MB)

For files up to 100 MB, upload is { "method": "PUT", "url", "headers", "expires_at" }. PUT the whole file to url and send every header in headers with it (they are part of the signature).

Upload a file up to 100 MB
curl -s https://api.zilapi.com/v1/files -H "Authorization: Bearer $ZILAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "talk.wav", "size_bytes": '"$(wc -c < talk.wav)"', "content_type": "audio/wav"}' \
  > upload.json
FILE_ID=$(jq -r .id upload.json)

# PUT the bytes to upload.url with every header in upload.headers
mapfile -t HEADERS < <(jq -r '.upload.headers | to_entries[] | "-H", "\(.key): \(.value)"' upload.json)
curl -X PUT "$(jq -r .upload.url upload.json)" "${HEADERS[@]}" --data-binary @talk.wav

curl -s -X POST https://api.zilapi.com/v1/files/$FILE_ID/complete -H "Authorization: Bearer $ZILAPI_KEY"
201 Created · POST /v1/files
{
  "id": "file_3Lm8QpZ2rT6w",
  "status": "uploading",
  "upload": {
    "method": "PUT",
    "url": "https://…r2.cloudflarestorage.com/…",
    "headers": { "Content-Type": "audio/wav" },
    "expires_at": "2026-10-06T20:02:07Z"
  }
}

Multipart upload (above 100 MB)

Above 100 MB, upload is { "method": "multipart", "part_size": 67108864, "parts": [{ "part_number", "url" }, …], "expires_at" }. Part n holds bytes (n − 1) × part_size up to n × part_size; the last part is shorter. PUT each part to its URL, keep the ETag response header, then finish with every part number and ETag.

Upload a file over 100 MB in parts
curl -s https://api.zilapi.com/v1/files -H "Authorization: Bearer $ZILAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "interview.wav", "size_bytes": '"$(wc -c < interview.wav)"', "content_type": "audio/wav"}' \
  > upload.json
FILE_ID=$(jq -r .id upload.json)
PART_SIZE=$(jq -r .upload.part_size upload.json)

# PUT each part_size slice to its URL and keep the ETag response header
: > parts.tsv
while IFS=$'\t' read -r N URL; do
  ETAG=$(dd if=interview.wav bs="$PART_SIZE" skip=$((N - 1)) count=1 status=none \
    | curl -s -X PUT -H "Content-Type:" --data-binary @- -D - -o /dev/null "$URL" \
    | tr -d '\r' | awk 'tolower($1) == "etag:" { print $2 }')
  printf '%s\t%s\n' "$N" "$ETAG" >> parts.tsv
done < <(jq -r '.upload.parts[] | [.part_number, .url] | @tsv' upload.json)

# Finish with every part number and its ETag
jq -R -s '{parts: [split("\n")[] | select(length > 0) | split("\t") | {part_number: (.[0] | tonumber), etag: .[1]}]}' parts.tsv \
  | curl -s -X POST https://api.zilapi.com/v1/files/$FILE_ID/complete -H "Authorization: Bearer $ZILAPI_KEY" \
      -H "Content-Type: application/json" --data-binary @-
POST /v1/files/{id}/complete body (multipart only)
{ "parts": [{ "part_number": 1, "etag": "\"9b2cf535f27731c974343645a3985328\"" }, { "part_number": 2, "etag": "\"…\"" }] }
Uploading from a browser? Upload from your backend, which holds the API key. Storage accepts browser uploads only from zilapi.com itself, so browser code on your own site cannot PUT to the upload URLs.

Wait until the file is ready

Validation is asynchronous. Poll every second or two, backing off, until the status is no longer validating. Creating a job with a file that is not ready gets 409 file_not_ready.

while :; do
  curl -s https://api.zilapi.com/v1/files/$FILE_ID -H "Authorization: Bearer $ZILAPI_KEY" > file.json
  STATUS=$(jq -r .status file.json)
  [ "$STATUS" != "validating" ] && break
  sleep 2
done
jq '{status, duration_seconds, error}' file.json   # "ready", 191.4, null

Limits and retention

  • Up to 5 GB (5,368,709,120 bytes) and 6 hours of audio per file; 25 MB (26,214,400 bytes) with a test key. Larger: 413 file_too_large. Longer: audio_too_long.
  • Accepted content_type: audio/wav, audio/mpeg, audio/mp4, audio/x-m4a, audio/aac, audio/flac, audio/ogg, audio/opus, audio/webm, audio/aiff, audio/amr and close variants, plus audio in video/mp4, video/webm, video/quicktime. Anything else: 415 unsupported_media_type. The real format is checked after upload.
  • Inputs are deleted 3 days after they become ready; results 7 days after the job succeeds.
  • Quotas per account: 10 open uploads, 20 GB of stored inputs and 30 validations per hour (1 GB and 5 per hour until your first top-up). Over a quota: 429 quota_exceeded.

Delete a file

Delete a file as soon as you no longer need it. A file that a queued or processing job uses cannot be deleted (409 file_in_use).

Delete a file now
curl -s -X DELETE https://api.zilapi.com/v1/files/$FILE_ID -H "Authorization: Bearer $ZILAPI_KEY" -o /dev/null -w "%{http_code}\n"   # 204