Upload Files API

How to Upload Files (API)

Upload images, videos, and audio directly to WaveSpeedAI storage. The API returns the same download_url used by model inputs, while the file bytes bypass the API gateway.

Quick start with cURL

The client makes one small authenticated request, then uploads the bytes to the returned short-lived URL. content_type is optional and size is calculated automatically below.

FILE="/path/to/your/image.png"
FILE_SIZE=$(wc -c < "$FILE" | tr -d ' ')
 
TICKET=$(curl --fail-with-body --silent --show-error \
  -X POST "https://api.wavespeed.ai/api/v3/media/uploads" \
  -H "Authorization: Bearer $WAVESPEED_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg filename "$(basename "$FILE")" --argjson size "$FILE_SIZE" \
    '{filename: $filename, size: $size}')")
 
UPLOAD_URL=$(printf '%s' "$TICKET" | jq -r '.data.upload.url')
UPLOAD_HEADERS=()
while IFS= read -r header; do
  UPLOAD_HEADERS+=(-H "$header")
done < <(printf '%s' "$TICKET" | jq -r \
  '.data.upload.headers | to_entries[] | "\(.key): \(.value)"')
 
curl --fail-with-body --request PUT "$UPLOAD_URL" \
  "${UPLOAD_HEADERS[@]}" \
  --upload-file "$FILE"
 
printf '%s' "$TICKET" | jq -r '.data.download_url'

The upload URL is a temporary credential. Do not log, persist, or share it. Send your API key only to api.wavespeed.ai; never add it to the PUT request.

Treat data.upload.url as an opaque, per-upload value. Its hostname and storage provider may change between requests or as WaveSpeedAI infrastructure evolves. Always use the complete URL returned for that upload; do not hardcode, construct, replace, or rewrite its hostname.

Create an upload

POST https://api.wavespeed.ai/api/v3/media/uploads
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
  "filename": "image.png",
  "size": 1024000,
  "content_type": "image/png"
}
FieldTypeRequiredDescription
filenamestringYesOriginal filename, including its extension
sizeintegerYesExact file size in bytes; maximum 200 MiB
content_typestringNoMIME type; inferred from filename when omitted

Response

{
  "code": 200,
  "message": "success",
  "data": {
    "type": "image",
    "download_url": "https://.../media/.../image.png",
    "filename": "image.png",
    "size": 1024000,
    "upload": {
      "method": "PUT",
      "url": "https://storage-provider.example/...signed...",
      "headers": {
        "Content-Type": "image/png",
        "If-None-Match": "*"
      },
      "expires_at": "2026-08-11T06:00:00Z"
    }
  }
}

Upload the exact file bytes with the returned method, URL, and headers. Do not add an Authorization header. Do not assume that a URL from a previous ticket, or its hostname, will be reused. A successful PUT returns any 2xx status. The download_url is ready to use immediately and has the same format as before.

Python example

import mimetypes
import os
from pathlib import Path
 
import requests
 
path = Path("/path/to/image.png")
payload = {"filename": path.name, "size": path.stat().st_size}
content_type = mimetypes.guess_type(path.name)[0]
if content_type:
    payload["content_type"] = content_type
 
ticket_response = requests.post(
    "https://api.wavespeed.ai/api/v3/media/uploads",
    headers={"Authorization": f"Bearer {os.environ['WAVESPEED_API_KEY']}"},
    json=payload,
    timeout=30,
)
ticket_response.raise_for_status()
ticket = ticket_response.json()["data"]
 
with path.open("rb") as file:
    upload_response = requests.put(
        ticket["upload"]["url"],
        headers=ticket["upload"]["headers"],
        data=file,
        timeout=300,
    )
upload_response.raise_for_status()
print(ticket["download_url"])

JavaScript example

import { readFile, stat } from 'node:fs/promises';
import { basename } from 'node:path';
 
const filePath = '/path/to/image.png';
const ticketResponse = await fetch('https://api.wavespeed.ai/api/v3/media/uploads', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.WAVESPEED_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filename: basename(filePath),
    size: (await stat(filePath)).size,
  }),
});
if (!ticketResponse.ok) throw new Error(await ticketResponse.text());
const ticket = (await ticketResponse.json()).data;
 
const uploadResponse = await fetch(ticket.upload.url, {
  method: ticket.upload.method,
  headers: ticket.upload.headers,
  body: await readFile(filePath),
});
if (!uploadResponse.ok) throw new Error(await uploadResponse.text());
console.log(ticket.download_url);

Errors and retention

CodeDescription
400Invalid filename, size, or content type
401Invalid API key
413File exceeds 200 MiB
429Upload quota or rate limit exceeded

The uploaded object must exactly match the declared size and content type. Upload tickets expire after a short period. Uploaded files are retained for 7 days and are intended only for WaveSpeedAI inference inputs.

Upload availability is governed by rolling safeguards based on account status and recent inference activity. Paid accounts may receive limited recovery capacity after reaching their normal allowance. These limits are adaptive, so integrations should handle quota responses and retry later rather than relying on fixed thresholds.

Legacy compatibility

Existing integrations may continue using POST /api/v3/media/upload/binary. It remains supported for compatibility, and integrations that still require the legacy flow do not need to migrate immediately. New integrations should use /api/v3/media/uploads because direct upload avoids routing file bytes through the API gateway and can provide better upload performance.

The simplest legacy request is multipart/form-data with a form field named file:

curl --fail-with-body --request POST \
  "https://api.wavespeed.ai/api/v3/media/upload/binary" \
  -H "Authorization: Bearer $WAVESPEED_API_KEY" \
  -F "file=@/path/to/image.png"

The legacy endpoint also accepts the file as the raw request body. Supply a specific Content-Type and either a file header containing the filename or an ext query parameter:

curl --fail-with-body --request POST \
  "https://api.wavespeed.ai/api/v3/media/upload/binary" \
  -H "Authorization: Bearer $WAVESPEED_API_KEY" \
  -H "Content-Type: image/png" \
  -H "file: image.png" \
  --data-binary "@/path/to/image.png"

A successful legacy request returns the media metadata directly:

{
  "code": 200,
  "message": "success",
  "data": {
    "type": "image",
    "download_url": "https://.../media/.../image.png",
    "filename": "image.png",
    "size": 1024000
  }
}

Use data.download_url as the model input, just as with the direct-upload flow. The same supported formats, 200 MiB single-file limit, retention policy, and upload quota rules apply. Unlike direct upload, the legacy request sends the file bytes through the WaveSpeedAI API gateway and completes in one HTTP request; there is no separate PUT step.

© 2026 WaveSpeedAI. All rights reserved.