# Files: Scarleta API reference

Page: https://www.scarleta.ai/docs/reference/files
Base URL: https://api.scarleta.ai. Authentication: `Authorization: Bearer $SCARLETA_API_KEY`.
Operations: 7.

## GET /v1/audio

Operation id: `list_files`

**List stored files**

Returns the files stored under your account, 100 per page by default, in storage order. Follow `pagination.cursor` while `pagination.has_more` is true. The list shows what is in storage: a file you deleted stays listed until it is removed for good (7 days after deletion, or when you delete it forever). Counts against the hourly account request budget. Available to free and paid accounts.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Files per page. Defaults to 100; values above 1000 are clamped to 1000, and a value below 1 or not a number falls back to the default. |
| `cursor` | query | string | no | The `pagination.cursor` of the previous page. Opaque. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `files` | array<object> |   |
| `files[].file_id` | string | Identifier of the file. |
| `files[].key` | string | Storage path of the file inside your account. |
| `files[].size` | integer | Size in bytes. Allowed: -9007199254740991 to 9007199254740991. |
| `files[].size_mb` | string | Size in mebibytes with two decimals. |
| `files[].uploaded` | string | When the file was stored, as an ISO 8601 timestamp. |
| `pagination` | object |   |
| `pagination.limit` | integer | The page size used. Allowed: -9007199254740991 to 9007199254740991. |
| `pagination.cursor` | string | Pass as `cursor` to get the next page. Absent on the last page. |
| `pagination.has_more` | boolean |   |

### Responses

200: One page of files. The body is `{ data }` without `meta`.

```json
{
  "data": {
    "files": [
      {
        "file_id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b",
        "key": "00000000-0000-4000-8000-000000000001/audio/3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav",
        "size": 176478,
        "size_mb": "0.17",
        "uploaded": "2026-10-20T11:53:45.938Z"
      }
    ],
    "pagination": {
      "limit": 100,
      "has_more": false
    }
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl "https://api.scarleta.ai/v1/audio" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## DELETE /v1/audio/{fileId}

Operation id: `delete_file`

**Delete a file**

Moves one of your files to the trash. It is not erased at once: it stays recoverable for 7 days (`purge_after`), after which it is removed for good. Only your own files can be deleted; any other id, and a file already in the trash, returns 404. Counts against the hourly account request budget. Available to free and paid accounts.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `fileId` | path | string | yes | The `file_id` of one of your files, as returned by the upload routes and the file list. A trailing media extension such as `.wav` is ignored. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `file_id` | string | Identifier of the file. |
| `status` | "trashed" |   |
| `trashed_at` | string | When the file was moved to the trash, as an ISO 8601 timestamp. |
| `purge_after` | string | When the file is removed for good, as an ISO 8601 timestamp. |

### Responses

200: The file is in the trash. The body is `{ data }` without `meta`.

```json
{
  "data": {
    "file_id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b",
    "status": "trashed",
    "trashed_at": "2026-10-21T03:40:58.310Z",
    "purge_after": "2026-10-28T03:40:58.310Z"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No file with this id in your account, or it is already in the trash.

```json
{
  "error": {
    "code": "file_not_found",
    "message": "Audio file not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#file_not_found"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`db_unavailable`](https://www.scarleta.ai/docs/errors#db_unavailable), [`file_not_found`](https://www.scarleta.ai/docs/errors#file_not_found), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
FILEID="YOUR_FILEID"
curl -X DELETE "https://api.scarleta.ai/v1/audio/$FILEID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/audio/{fileId}/download-url

Operation id: `create_download_url`

**Create a download link**

Creates a link for downloading one of your files. Fetch `download_url` with a plain `GET` and no API key; the response is the file with a `Content-Disposition: attachment` header. The link works once, and only until `expires_in` seconds have passed (60 to 86400; 3600 if you send no body). A second request, or one after expiry, gets 403 `invalid_download_token`; create a new link instead. Only your own files can be linked: any other id returns 404.

Send `filename` to choose the name the download is saved under (ignored when longer than 200 characters). The body is optional; one that is not valid JSON is treated as empty. Counts against the hourly account request budget. Available to free and paid accounts.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `fileId` | path | string | yes | The `file_id` of one of your files, as returned by the upload routes and the file list. A trailing media extension such as `.wav` is ignored. |

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `expires_in` | integer | Seconds the link stays valid, 60 to 86400. Defaults to 3600. |
| `filename` | string | File name for the download, at most 200 characters. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `download_url` | string | Address to `GET` the file from. Works once. |
| `file_id` | string | Identifier of the file. |
| `expires_in` | integer | Seconds the link stays valid. Allowed: -9007199254740991 to 9007199254740991. |

### Responses

200: Link created. The body is `{ data }` without `meta`.

```json
{
  "data": {
    "download_url": "https://api.scarleta.ai/v1/audio/download/dt_v1_859fca05-4d2f-41ce-89b7-bd1d8681adc9?sig=632d9afd5adb5160267986aa97911c50&fn=morning-session.wav",
    "file_id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b",
    "expires_in": 3600
  }
}
```

400: `expires_in` is not an integer in range.

```json
{
  "error": {
    "code": "invalid_expires_in",
    "message": "expires_in must be an integer between 60 and 86400",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_expires_in"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No file with this id in your account.

```json
{
  "error": {
    "code": "file_not_found",
    "message": "Audio file not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#file_not_found"
  }
}
```

413: The stored file is over the size limit and cannot be linked.

```json
{
  "error": {
    "code": "file_too_large",
    "message": "Uploaded file exceeds the per-file size limit",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#file_too_large"
  }
}
```

415: The stored file is not audio and cannot be linked.

```json
{
  "error": {
    "code": "invalid_audio_file",
    "message": "Stored file is not a supported audio/media format",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_audio_file"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`file_not_found`](https://www.scarleta.ai/docs/errors#file_not_found), [`file_too_large`](https://www.scarleta.ai/docs/errors#file_too_large), [`invalid_audio_file`](https://www.scarleta.ai/docs/errors#invalid_audio_file), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_expires_in`](https://www.scarleta.ai/docs/errors#invalid_expires_in), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
FILEID="YOUR_FILEID"
curl -X POST "https://api.scarleta.ai/v1/audio/$FILEID/download-url" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expires_in": 3600,
    "filename": "morning-session.wav"
  }'
```

## GET /v1/audio/storage-usage

Operation id: `get_storage_usage`

**Get storage usage**

Returns how much storage your account uses. Every account includes 1 TiB; `cap_bytes` reports that included amount. Files in the trash still count until they are removed for good. Counts against the hourly account request budget. Available to free and paid accounts.

Access: API key required; free and paid accounts. Cost: no token cost.

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `used_bytes` | integer | Bytes your files use. Allowed: -9007199254740991 to 9007199254740991. |
| `cap_bytes` | integer | Bytes of storage included with the account. Allowed: -9007199254740991 to 9007199254740991. |
| `tier` | "free" \| "paid" | The account tier the figures are for. |

### Responses

200: Usage figures. The body is `{ data }` without `meta`.

```json
{
  "data": {
    "used_bytes": 208773279,
    "cap_bytes": 1099511627776,
    "tier": "paid"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl "https://api.scarleta.ai/v1/audio/storage-usage" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/audio/upload-url

Operation id: `create_upload_url`

**Get a presigned upload URL**

Starts an upload that goes straight to storage. Declare the file with `content_length` (and `content_type`), then send the file bytes with a `PUT` to the returned `upload_url` within 15 minutes. The file then appears in your library and the file list. A file can be up to 10 GiB for audio and 1 GiB for MP4 or MOV video.

`upload_url` is opaque: send the request to it exactly as returned, with the same `Content-Type` you declared, and do not parse it or add your API key (the URL itself is the credential). When `direct` is `false` the URL points back at this API instead of storage; the same `PUT` rules apply. Use `media_url` to refer to the stored file afterwards, and `audio_id` to attach it as a version.

Declaring the size reserves that much of your storage right away, so a file that would not fit is refused here with 413 `storage_quota_exceeded`, before any bytes are sent. An upload you never complete keeps its reservation until it is cleaned up. For files you want to resume, or for many parts in parallel, use an upload session instead. Counts against the hourly account request budget. Available to free and paid accounts.

Access: API key required; free and paid accounts. Cost: no token cost.

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `content_length` | integer | Size of the file in bytes. Required. At most 10 GiB for audio and 1 GiB for video. `size` is accepted as an alias. Allowed: at most 10737418240. |
| `content_type` | string | Content type of the file. Defaults to `audio/wav`. One of: `audio/wav`, `audio/wave`, `audio/x-wav`, `audio/mpeg`, `audio/mp3`, `audio/ogg`, `audio/flac`, `audio/aac`, `audio/m4a`, `audio/mp4`, `audio/x-m4a`, `audio/aiff`, `audio/x-aiff`, `audio/webm`, `audio/opus`, `audio/x-opus+ogg`, `video/mp4`, `video/quicktime`. |
| `filename` | string | Name to show for the file, at most 255 characters. Path separators and control characters are removed. |
| `duration_seconds` | number | Length of the audio in seconds, if you know it. Display only: it is never used for billing. Values outside 0 to 86400 are ignored. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `upload_url` | string | Where to `PUT` the file bytes. Opaque; use it as returned. |
| `file_id` | string | Identifier of the new file. |
| `key` | string | Storage path of the file inside your account. |
| `expires_in` | integer | Seconds `upload_url` stays valid: 900. Allowed: -9007199254740991 to 9007199254740991. |
| `content_type` | string | The content type to send with the `PUT`. |
| `filename` | string |   |
| `direct` | boolean | `true` when `upload_url` goes straight to storage; `false` when it points back at this API. |
| `media_url` | string | Permanent address of the file once uploaded. Use it to refer to the file in other requests. |
| `playback_url` | string | Signed address for playing the file in a browser; it expires, `media_url` does not. |
| `audio_id` | string \| null | Library identifier of the file, used to attach it as a version of another file. `null` if it could not be computed. |

### Responses

200: Upload prepared. The body is `{ data }` without `meta`.

```json
{
  "data": {
    "upload_url": "https://storage.example.com/uploads/3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav?signature=…",
    "file_id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b",
    "key": "00000000-0000-4000-8000-000000000001/audio/3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav",
    "expires_in": 900,
    "content_type": "audio/wav",
    "filename": "morning-session.wav",
    "direct": true,
    "media_url": "https://api.scarleta.ai/v1/audio/media/3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav",
    "playback_url": "https://api.scarleta.ai/v1/audio/media/3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav?exp=1792557639&sig=9f2c1d4e8a7b6c5d",
    "audio_id": "audio_bbb14949dc19"
  }
}
```

400: No positive `content_length`, or a content type that is not one of the accepted ones.

```json
{
  "error": {
    "code": "missing_content_length",
    "message": "content_length or size is required and must be a positive integer",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#missing_content_length"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

413: The declared size is over the per-file limit, or the file does not fit in your storage (`storage_quota_exceeded`).

```json
{
  "error": {
    "code": "file_too_large",
    "message": "This file is larger than 10 GB, the most a single file can be.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#file_too_large"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: The upload could not be registered; nothing was reserved. Retry shortly.

```json
{
  "error": {
    "code": "upload_unavailable",
    "message": "Upload couldn't start. Try again.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#upload_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`file_too_large`](https://www.scarleta.ai/docs/errors#file_too_large), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_content_type`](https://www.scarleta.ai/docs/errors#invalid_content_type), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`missing_content_length`](https://www.scarleta.ai/docs/errors#missing_content_length), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`storage_quota_exceeded`](https://www.scarleta.ai/docs/errors#storage_quota_exceeded), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`upload_unavailable`](https://www.scarleta.ai/docs/errors#upload_unavailable)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/audio/upload-url" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content_type": "audio/wav",
    "filename": "morning-session.wav",
    "content_length": 1920044,
    "duration_seconds": 60
  }'
```

## PUT /v1/audio/upload/{fileId}

Operation id: `upload_file_bytes`

**Upload file bytes through the API**

Fallback target of a presigned upload. When the upload URL from the presigned upload route has `direct: false`, it points here: it already carries the one-time `t` token, so call it exactly as returned, with the file bytes as the request body and the `Content-Type` you declared. There is no API key on this route; the token is the credential, it works once, and a URL that is expired, used or wrong gets 403 `invalid_upload_intent`.

The body is read in one piece, so the limit is 100 MiB for audio. The bytes are checked: a file that is not valid audio is refused with 400 `invalid_file_signature`. Uploads are limited to 30 per hour per source address. Most accounts receive `direct: true` URLs and never call this route.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `fileId` | path | string | yes | The `file_id` of one of your files, as returned by the upload routes and the file list. A trailing media extension such as `.wav` is ignored. |
| `t` | query | string | no | One-time upload token. Already part of the `upload_url` that points here; leave it as returned. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `file_id` | string | Identifier of the file. |
| `key` | string | Storage path of the file inside your account. |
| `status` | "uploaded" |   |
| `uploaded_at` | string | ISO 8601 timestamp. |

### Responses

201: The file is stored. The body is `{ data }` without `meta`.

```json
{
  "data": {
    "file_id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b",
    "key": "00000000-0000-4000-8000-000000000001/audio/3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav",
    "status": "uploaded",
    "uploaded_at": "2026-10-20T11:53:45.938Z"
  }
}
```

400: The body is empty, the content type is not accepted, or the file is not audio.

```json
{
  "error": {
    "code": "invalid_file_signature",
    "message": "File does not appear to be a valid audio file",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_file_signature"
  }
}
```

403: The URL has expired, was already used, or the token is missing or wrong.

```json
{
  "error": {
    "code": "invalid_upload_intent",
    "message": "Upload URL has expired or is invalid",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_upload_intent"
  }
}
```

413: The body is larger than this route accepts.

```json
{
  "error": {
    "code": "file_too_large",
    "message": "File size exceeds maximum allowed size",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#file_too_large"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

### Error codes

[`file_too_large`](https://www.scarleta.ai/docs/errors#file_too_large), [`invalid_content_type`](https://www.scarleta.ai/docs/errors#invalid_content_type), [`invalid_file_signature`](https://www.scarleta.ai/docs/errors#invalid_file_signature), [`invalid_upload_intent`](https://www.scarleta.ai/docs/errors#invalid_upload_intent), [`missing_body`](https://www.scarleta.ai/docs/errors#missing_body), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable)

### Example

```bash
FILEID="YOUR_FILEID"
curl -X PUT "https://api.scarleta.ai/v1/audio/upload/$FILEID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: audio/wav" \
  --data-binary @./recording.wav
```

## POST /v1/upload

Operation id: `upload_file`

**Upload a file for one-off use**

Uploads one audio file in a single request and returns a public `url` you can pass as `audio_url` to the analysis routes. Send `multipart/form-data` with the audio in the `file` part. Audio only: video is refused here, use the presigned upload route instead. The bytes are checked, not the declared type: a file that is not valid audio is refused with 400 `invalid_file_signature`. The body is read in one piece, so the limit is 100 MiB; larger files, and anything you want to keep in your library, go through the presigned upload route or an upload session.

The file is temporary: it is removed 24 hours after upload. While it exists its size counts toward your storage. `url` never expires within that time and is the value to use as an `audio_url`; `playback_url` is a signed copy for playing the file in a browser and stops working sooner.

This route answers with a bare JSON object: there is no `data` wrapper, no `meta` on a success. Uploads are limited to 30 per hour per account and per source address. Available to free and paid accounts.

Access: API key required; free and paid accounts. Cost: no token cost.

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `file` | string | The audio file, at most 100 MiB. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `success` | true |   |
| `url` | string | Public URL of the stored file. Pass it as `audio_url`. It does not expire while the file exists. |
| `playback_url` | string | Signed variant of `url` for playing the file in a browser; it expires sooner than the file does. |
| `file_id` | string | Identifier of the stored file. |
| `filename` | string | Name the file is stored under. |
| `size` | integer | Size in bytes. Allowed: -9007199254740991 to 9007199254740991. |
| `content_type` | string | Content type the upload declared, or `application/octet-stream`. |
| `expires_in` | integer | Seconds the file is kept: 86400. Allowed: -9007199254740991 to 9007199254740991. |
| `expires_at` | string | When the file is removed, as an ISO 8601 timestamp. |

### Responses

200: The file is stored. The body is the bare object shown, without a `data` wrapper.

```json
{
  "success": true,
  "url": "https://api.scarleta.ai/audio-cache/temp-3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav",
  "playback_url": "https://api.scarleta.ai/audio-cache/temp-3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav?exp=1792557639&sig=9f2c1d4e8a7b6c5d",
  "file_id": "3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b",
  "filename": "temp-3f2b8c1e-5d4a-4e6f-9a7b-1c2d3e4f5a6b.wav",
  "size": 176478,
  "content_type": "audio/wav",
  "expires_in": 86400,
  "expires_at": "2026-10-22T03:40:39.370Z"
}
```

400: The request is not `multipart/form-data`, has no `file` part, or the file is not audio (checked by its bytes, not its declared type), or is a video.

```json
{
  "error": {
    "code": "invalid_file_signature",
    "message": "File does not appear to be a valid audio file",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_file_signature"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

413: The file is larger than 100 MiB.

```json
{
  "error": {
    "code": "file_too_large",
    "message": "File size exceeds maximum allowed size",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#file_too_large"
  }
}
```

429: Over the account request budget. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`file_too_large`](https://www.scarleta.ai/docs/errors#file_too_large), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_content_type`](https://www.scarleta.ai/docs/errors#invalid_content_type), [`invalid_file_signature`](https://www.scarleta.ai/docs/errors#invalid_file_signature), [`invalid_file_type`](https://www.scarleta.ai/docs/errors#invalid_file_type), [`invalid_form_data`](https://www.scarleta.ai/docs/errors#invalid_form_data), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`missing_file`](https://www.scarleta.ai/docs/errors#missing_file), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`video_not_supported_here`](https://www.scarleta.ai/docs/errors#video_not_supported_here)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/upload" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -F "file=@./recording.wav;type=audio/wav"
```
