# Uploads

> Send files up to 95 MB in one request, or up to 2 GB in resumable chunks.

Canonical: https://transcript1.com/docs/uploads
Last updated: 2026-10-14

Upload audio or video files for AI speech-to-text. For uploads, `source` can only be `auto` or `stt`. See [Sources and options](https://transcript1.com/docs/sources-and-options) for `language` and `diarize`.

## Supported files

The file name must end in one of these extensions: `mp3`, `wav`, `m4a`, `flac`, `ogg`, `opus`, `mp4`, `mov`, `webm` or `mkv`. Anything else returns `422 unsupported_media`. The file must contain an audio track; a file without one fails with `unsupported_media`.

| Limit | Value |
|---|---|
| One-shot upload (`multipart/form-data`) | 95 MB |
| Chunked upload | 2 GB (2,000,000,000 bytes). Your plan may lower this. |
| Open chunked uploads per team | 20 |

A file over the limit returns `413 media_too_large` with `max_bytes` in the error.

## One-shot upload

For files up to 95 MB, send one `multipart/form-data` request to `POST /v1/transcriptions`. The file goes in a part named `file`. These optional form fields are also accepted: `language`, `diarize` (`true` or `false`), `source` (`auto` or `stt`) and `webhook_url`.

```bash
curl -X POST "https://api.transcript1.com/v1/transcriptions?wait=60" \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -F file=@interview.m4a \
  -F language=en \
  -F diarize=true
```

The response contains a transcription object with `source.platform` set to `upload`. The status code is `202` while processing and `200` at a final status, as for a video link. An empty file returns `422 invalid_request`.

## Chunked upload

Send large files in separate chunks so you can resume an interrupted upload. Follow these four steps.

### 1. Create the upload

Send the file's name and size as JSON. `mime_type` is optional. Add an `Idempotency-Key` header so you can safely retry the create:

```bash
curl -X POST "https://api.transcript1.com/v1/transcriptions" \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Idempotency-Key: interview-2026-10-13" \
  -H "Content-Type: application/json" \
  -d '{"upload":{"filename":"interview.m4a","size":48210334,"mime_type":"audio/mp4"},"language":"auto","diarize":true}'
```

The response is `201` with the status `awaiting_upload` and an `upload` block:

```json
{
  "id": "tr_8fK2Lm0PqRsTuVwXyZaBcD",
  "object": "transcription",
  "status": "awaiting_upload",
  "upload": {
    "chunk_url_template": "https://api.transcript1.com/v1/transcriptions/tr_8fK2Lm0PqRsTuVwXyZaBcD/upload/chunks/{index}",
    "headers": { "X-Upload-Token": "t1u_..." },
    "expires_at": "2026-10-13T10:30:00.000Z",
    "chunks": [
      { "index": 0, "offset": 0, "size": 8388608 },
      { "index": 1, "offset": 8388608, "size": 8388608 }
    ]
  }
}
```

The server plans the chunks, about 8 MiB each (8,388,608 bytes), and returns the list in `chunks` (shortened above). The `X-Upload-Token` is a secret for this one upload and is valid for 1 hour (`expires_at`).

### 2. Send each chunk

Send every chunk with `PUT` to `chunk_url_template`, replacing `{index}` with the chunk's `index`. The body is the bytes from `offset` for `size` bytes, sent as `application/octet-stream`. The body must be exactly the planned size. Authenticate with the `X-Upload-Token` header; no `Authorization` header is needed.

```bash
curl -X PUT "https://api.transcript1.com/v1/transcriptions/tr_8fK2Lm0PqRsTuVwXyZaBcD/upload/chunks/0" \
  -H "X-Upload-Token: t1u_..." \
  -H "Content-Type: application/octet-stream" \
  --data-binary @chunk-0.bin
```

Each chunk returns `{ "object": "upload_chunk", "index": 0, "size": 8388608 }`. Sending a chunk again replaces it. A wrong size or an index outside the plan returns `422 invalid_request`. A missing or expired token returns `401 unauthorized`.

### 3. Resume if interrupted

`GET /v1/transcriptions/:id/upload` lists what arrived:

```json
{
  "object": "upload",
  "status": "awaiting_upload",
  "total_bytes": 48210334,
  "uploaded_bytes": 16777216,
  "received_chunks": [0, 1],
  "chunks": [ { "index": 0, "offset": 0, "size": 8388608 } ],
  "expires_at": "2026-10-13T10:30:00.000Z"
}
```

Send only the chunks missing from `received_chunks`. If the token expired, repeat the create request from step 1 with the same `Idempotency-Key` and parameters. The response returns the same transcription with a new upload token.

### 4. Complete the upload

```bash
curl -X POST "https://api.transcript1.com/v1/transcriptions/tr_8fK2Lm0PqRsTuVwXyZaBcD/upload/complete" \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

`POST /v1/transcriptions/:id/upload/complete` checks that every planned chunk arrived with its planned size, assembles the file and queues the transcription. It returns `202` with the transcription object. If chunks are missing it returns `409 upload_incomplete` with `missing_chunks`, a list of the chunk indexes to send again. If the upload was already completed or canceled it returns `409 invalid_state`. From here, poll with `GET /v1/transcriptions/:id?wait=60` or use a [webhook](https://transcript1.com/docs/webhooks).

### Complete Python example

```python
import os
import requests

API = "https://api.transcript1.com"
auth = {"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"}
path = "interview.m4a"

created = requests.post(
    f"{API}/v1/transcriptions",
    headers={**auth, "Idempotency-Key": "interview-2026-10-13"},
    json={"upload": {"filename": os.path.basename(path), "size": os.path.getsize(path)}},
)
created.raise_for_status()
transcription = created.json()
upload = transcription["upload"]

with open(path, "rb") as file:
    for chunk in upload["chunks"]:
        file.seek(chunk["offset"])
        url = upload["chunk_url_template"].replace("{index}", str(chunk["index"]))
        response = requests.put(
            url,
            data=file.read(chunk["size"]),
            headers={**upload["headers"], "Content-Type": "application/octet-stream"},
        )
        response.raise_for_status()

done = requests.post(f"{API}/v1/transcriptions/{transcription['id']}/upload/complete", headers=auth)
done.raise_for_status()
print(done.json()["status"])
```

## Limits and retention

- A team can have at most 20 uploads open (status `awaiting_upload`) at once. Creating more returns `429 too_many_active_jobs` with a `Retry-After` header.
- An upload that is not completed within 24 hours of being created is canceled, and its chunks are deleted.
- Your media is deleted 24 hours after the transcription reaches a final status (`completed`, `failed` or `canceled`). The transcript itself is kept until you delete it with `DELETE /v1/transcriptions/:id`.
- Failed and canceled transcriptions are never charged. Minutes are charged per media minute, rounded up, once the file's duration is known. See [Billing and limits](https://transcript1.com/docs/billing-and-limits).
