> For the complete documentation index, see [llms.txt](https://docs.castmagic.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.castmagic.io/endpoints/uploads.md).

# Uploads

Upload a media file directly to storage, then turn it into a recording.

Uploading is a two-step, direct-to-storage flow:

1. `POST /v1/uploads` to reserve an object and get a signed `upload_url`.
2. `PUT` the raw file bytes to `upload_url`.
3. Call [`POST /v1/recordings`](/endpoints/recordings.md#create-a-recording) with the returned `object_id` to create the recording and start transcription.

{% hint style="info" %}
**Tier:** `core`. Base URL `https://app.castmagic.io`.
{% endhint %}

## Start an upload

<mark style="color:green;">`POST`</mark> `https://app.castmagic.io/v1/uploads`

#### Request Body

| Name                                            | Type    | Description                             |
| ----------------------------------------------- | ------- | --------------------------------------- |
| filename<mark style="color:red;">\*</mark>      | String  | Name of the file, e.g. `interview.m4a`  |
| content\_type<mark style="color:red;">\*</mark> | String  | MIME type, e.g. `audio/mp4`             |
| content\_size<mark style="color:red;">\*</mark> | Integer | Size in bytes                           |
| space\_id                                       | String  | Defaults to the key's default space     |
| title                                           | String  | Optional title carried to the recording |

{% tabs %}
{% tab title="201: Created" %}

```json
{
    "object_id": "obj_...",
    "upload_url": "https://storage.googleapis.com/...",
    "upload_url_expires_in_seconds": 1800,
    "space_id": "a1..."
}
```

{% endtab %}

{% tab title="402: Payment Required Storage limit reached" %}

```json
{ "error": "storage limit exceeded" }
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Files are capped at **10 GB**. Uploads that would exceed the plan's storage hard limit are rejected with `402`. The signed `upload_url` is valid for **30 minutes** (`upload_url_expires_in_seconds`).
{% endhint %}

## PUT the file bytes

Upload the raw bytes to the returned `upload_url` with an HTTP `PUT`. Use the same `Content-Type` you declared:

```bash
curl -X PUT \
    -H "Content-Type: audio/mp4" \
    --data-binary @interview.m4a \
    "$UPLOAD_URL"
```

This request goes straight to storage — it does **not** carry your Castmagic `Authorization` header. Once the bytes are uploaded, call [`POST /v1/recordings`](/endpoints/recordings.md#create-a-recording) with the `object_id`. Castmagic verifies the bytes actually arrived before creating the recording; if they never did, that call returns `422`.
