> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sunra.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# File Upload

Sunra models take their file inputs as URLs — the audio you want transcribed, the image you want animated. When the file only exists on your machine or in a private bucket, upload it to Sunra storage first: you get back an `assets.sunra.ai` URL that you can pass straight into any model endpoint.

Uploading takes two requests:

1. **Initiate** — `POST /v1/storage/upload/initiate` with the file name and content type. Sunra replies with a pre-signed URL to upload to, plus the final public URL the file will have.
2. **Upload** — `PUT` the bytes to that pre-signed URL.

The file is readable at its public URL as soon as the `PUT` succeeds — there is no processing step to wait for.

## Authentication

Only the first request is authenticated, with the same API key you use everywhere else on `api.sunra.ai`:

```bash theme={null}
Authorization: Key $SUNRA_KEY
```

The second request carries its authorization inside the pre-signed URL, so it needs no API key — which is what makes it safe to hand the `upload_url` to a browser or a mobile client. See [Authentication](/platform/authentication) for how to obtain and store the key.

## Step 1 — Initiate the upload

```bash theme={null}
curl -X POST https://api.sunra.ai/v1/storage/upload/initiate \
  -H "Authorization: Key $SUNRA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "chunk10.mp3", "content_type": "audio/mpeg"}'
```

### Request body

| Field          | Type   | Required | Description                                                                                                                                                                                                            |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file_name`    | string | yes      | Name of the file **including its extension**, for example `chunk10.mp3`. Only the extension is carried over to the public URL. Pass a bare file name: path separators are rejected, so `audio/chunk10.mp3` is a `400`. |
| `content_type` | string | yes      | MIME type of the file, for example `audio/mpeg`. You must send this same value as the `Content-Type` header when you upload.                                                                                           |

### Response

`201 Created`:

```json theme={null}
{
  "upload_url": "https://<object-storage-host>/uploads/2f8c1d9e-6b4a-4d21-9f0e-7c3ab5d81e64.mp3?X-Amz-Algorithm=...&X-Amz-Signature=...",
  "file_url": "https://assets.sunra.ai/uploads/2f8c1d9e-6b4a-4d21-9f0e-7c3ab5d81e64.mp3"
}
```

| Field        | Description                                                                                                  |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| `upload_url` | Pre-signed `PUT` URL for the object store. Valid for **1 hour** (3600 seconds) from the moment it is issued. |
| `file_url`   | Public URL the file will have once uploaded. This is the value you pass to model endpoints.                  |

Each call mints a fresh, randomly named object — the name you send is never used as the object name, so uploads can never collide with or overwrite each other. Only the extension survives, lower-cased: `Chunk10.MP3` lands at `…/<uuid>.mp3`.

### Rejected file names

Both of these are `400` and create nothing, so correct the name and call again:

| Error message                | Cause                                                                                                                                        |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `Invalid filename`           | Empty, longer than 255 characters, or containing a path separator or a control character — for example `nested/file.mp3` or `../escape.mp3`. |
| `Invalid filename extension` | No extension at all (`recording`), a name that is only an extension (`.mp3`), or an extension that is not 1–10 alphanumeric characters.      |

Multi-part extensions keep only the last segment: `archive.tar.gz` uploads fine and lands at `…/<uuid>.gz`.

## Step 2 — Upload the file

`PUT` the raw bytes to `upload_url`, repeating the content type you declared:

```bash theme={null}
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @chunk10.mp3
```

The `Content-Type` header is part of what was signed. If it differs from the `content_type` you sent to `/upload/initiate` — even by case or by a stray charset parameter — the signature no longer matches and the object store rejects the upload. Do not add an `Authorization` header here.

## Step 3 — Use the file URL

`file_url` is a normal public URL. Pass it to any model endpoint that accepts a file input — here, speech-to-text:

```bash theme={null}
curl -X POST https://api.sunra.ai/v1/queue/bytedance/seed-asr-2.0/speech-to-text \
  -H "Authorization: Key $SUNRA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"audio": "https://assets.sunra.ai/uploads/2f8c1d9e-6b4a-4d21-9f0e-7c3ab5d81e64.mp3"}'
```

That returns a `request_id` like any other submission; poll or stream it as described in [Queue](/multimodal/queue).

## Full example

Upload a local audio file and transcribe it:

<CodeGroup>
  ```bash Curl theme={null}
  #!/usr/bin/env bash
  set -euo pipefail

  FILE="chunk10.mp3"
  CONTENT_TYPE="audio/mpeg"

  # 1. Ask for a pre-signed URL
  RESPONSE=$(curl -sS --fail-with-body -X POST https://api.sunra.ai/v1/storage/upload/initiate \
    -H "Authorization: Key $SUNRA_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg name "$(basename "$FILE")" --arg type "$CONTENT_TYPE" \
          '{file_name: $name, content_type: $type}')")

  UPLOAD_URL=$(echo "$RESPONSE" | jq -r .upload_url)
  FILE_URL=$(echo "$RESPONSE" | jq -r .file_url)

  # 2. Upload the bytes
  curl -sS --fail-with-body -X PUT "$UPLOAD_URL" \
    -H "Content-Type: $CONTENT_TYPE" \
    --data-binary "@$FILE"

  # 3. Feed the public URL to a model
  curl -sS --fail-with-body -X POST https://api.sunra.ai/v1/queue/bytedance/seed-asr-2.0/speech-to-text \
    -H "Authorization: Key $SUNRA_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg url "$FILE_URL" '{audio: $url}')"
  ```

  ```python Python theme={null}
  import os

  import requests

  API_KEY = os.environ["SUNRA_KEY"]
  BASE_URL = "https://api.sunra.ai"


  def upload_file(path: str, content_type: str) -> str:
      """Upload a local file to Sunra storage and return its public URL."""
      initiated = requests.post(
          f"{BASE_URL}/v1/storage/upload/initiate",
          headers={"Authorization": f"Key {API_KEY}"},
          json={
              "file_name": os.path.basename(path),
              "content_type": content_type,
          },
      )
      initiated.raise_for_status()
      upload_url = initiated.json()["upload_url"]
      file_url = initiated.json()["file_url"]

      with open(path, "rb") as handle:
          uploaded = requests.put(
              upload_url,
              data=handle,
              headers={"Content-Type": content_type},
          )
      uploaded.raise_for_status()

      return file_url


  audio_url = upload_file("chunk10.mp3", "audio/mpeg")

  submitted = requests.post(
      f"{BASE_URL}/v1/queue/bytedance/seed-asr-2.0/speech-to-text",
      headers={
          "Authorization": f"Key {API_KEY}",
          "Content-Type": "application/json",
      },
      json={"audio": audio_url},
  )
  submitted.raise_for_status()
  print(submitted.json())
  ```
</CodeGroup>

## Things to know

* **The extension is required, and it is load-bearing.** It is the one part of `file_name` that survives into `file_url`, and some models — speech-to-text in particular — read the media format off the URL path. Send the real extension for the file's actual format; a WAV file named `.mp3` will reach the model mislabelled.
* **`upload_url` expires after one hour.** It is a signed URL, not a reservation: if it expires before you finish, call `/upload/initiate` again. Each call mints a fresh object, so the retry gets a new `file_url` too.
* **Content type must match on both requests.** The value in `content_type` and the `Content-Type` header of the `PUT` have to be identical.
* **Uploaded files are publicly readable.** `file_url` is unguessable, but it is not secret and it is not time-limited — anyone who has the URL can fetch the file. Do not upload anything confidential, and treat the URLs themselves as sensitive if the content is.
* **Files are not tied to a request.** Once uploaded, a `file_url` can be reused across as many predictions as you like; it is not consumed by the first model that reads it.
