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
POST /v1/fileswithfilename,size_bytesandcontent_type. You get a file id (status: uploading) and upload instructions inupload.- 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. POST /v1/files/{id}/complete. It answers202withstatus: validating; for multipart uploads, send the parts and their ETags.- Poll
GET /v1/files/{id}untilstatusisready(withduration_seconds) orrejected(witherror).
| Status | Meaning |
|---|---|
| uploading | Waiting for the bytes and /complete. The upload URLs are valid for 6 hours. |
| validating | We copy the upload and measure the audio. Usually a few seconds. |
| ready | Usable in jobs. duration_seconds is set. Deleted 3 days later (expires_at). |
| rejected | Not audio we can decode (invalid_audio) or over 6 hours (audio_too_long). See error. |
| deleted | Removed 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).
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"{
"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.
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 @-{ "parts": [{ "part_number": 1, "etag": "\"9b2cf535f27731c974343645a3985328\"" }, { "part_number": 2, "etag": "\"…\"" }] }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, nullLimits 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/amrand close variants, plus audio invideo/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).
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