# API reference

> Every /v1 endpoint with parameters and examples. Download the OpenAPI spec as JSON or YAML.

Canonical: https://transcript1.com/docs/api-reference
Last updated: 2026-10-19

## Introduction

The base URL is `https://api.transcript1.com/v1`.

- Request and response bodies are JSON with `snake_case` field names.
- Timestamps are ISO 8601 in UTC, for example `2026-10-13T09:30:00.000Z`.
- Durations and transcript times are in seconds.
- Every error uses the same structure. See [Errors](https://transcript1.com/docs/api-reference#errors).
- Every response to an authenticated request carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. See [Billing and limits](https://transcript1.com/docs/billing-and-limits).
- `source=auto` prefers usable original-language captions on YouTube, X, TikTok, Bluesky, Dailymotion and LinkedIn, otherwise speech-to-text. Not every video has captions. `source=captions` never downloads audio or falls back to STT; no usable track means `no_captions`. `source=stt` and speaker labels bypass captions. See the [source capability matrix](https://transcript1.com/docs/sources-and-options) for retrieval restrictions and segment versus word timing.

Download the machine-readable OpenAPI spec:

- [JSON](https://api.transcript1.com/v1/openapi.json)
- [YAML](https://api.transcript1.com/v1/openapi.yaml)

## Authentication

Send a credential in the `Authorization` header: `Authorization: Bearer <token>`.

- An API key starts with `t1_`. Create one on the [API keys page](https://transcript1.com/dashboard/account/tokens). API keys can call every endpoint. `POST /v1/topups` charges the saved card only for a key with **Can buy top-ups** switched on; it is off by default.
- An OAuth access token starts with `t1o_`. MCP clients such as Claude and ChatGPT receive one when you connect them. Its scopes are `transcriptions:read` and `transcriptions:write`, plus `billing:write` only when you allow it.

| Scope | Allows |
|---|---|
| `transcriptions:read` | `GET /v1/transcriptions`, `GET /v1/transcriptions/:id`, `GET /v1/transcriptions/:id/upload` and `GET /v1/account` |
| `transcriptions:write` | `POST /v1/transcriptions`, `DELETE /v1/transcriptions/:id`, `POST /v1/transcriptions/:id/upload/complete` and `POST /v1/topups` |
| `billing:write` | Lets `POST /v1/topups` charge the saved card, up to the monthly agent limit. Never granted by default: a client must ask for it, the consent screen shows it unchecked, and you can switch it on or off later with **Can buy top-ups** on the [Connected apps page](https://transcript1.com/dashboard/account/connected-apps). Without it, `POST /v1/topups` returns `402 topup_needs_user`. |

- A token without the required scope returns `403 insufficient_scope`.
- A missing or invalid credential returns `401 unauthorized`.

For chunk uploads, authenticate `PUT /v1/transcriptions/:id/upload/chunks/:index` with `X-Upload-Token`. You receive this token when you create the upload. See [Uploads](https://transcript1.com/docs/uploads).

```bash
curl https://api.transcript1.com/v1/account \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1({ apiKey: process.env.TRANSCRIPT1_API_KEY });
const account = await t1.account();
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/account",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

## Idempotency

To retry `POST /v1/transcriptions` safely, send an `Idempotency-Key` request header.

The value must be 1 to 255 characters. Send it as a header, not a query parameter.

- The same key and parameters return the original transcription without another charge.
- The same key with different parameters returns `409 idempotency_conflict`.
- For chunked uploads, repeating the create request with the same key issues a fresh upload token.

```bash
curl -X POST https://api.transcript1.com/v1/transcriptions \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Idempotency-Key: 5f1c2b0e-8d4a-4c3e-9b7a-2e6f1d0c9a8b" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const transcription = await t1.transcriptions.create('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
  idempotencyKey: '5f1c2b0e-8d4a-4c3e-9b7a-2e6f1d0c9a8b',
});
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/transcriptions",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}", "Idempotency-Key": "5f1c2b0e-8d4a-4c3e-9b7a-2e6f1d0c9a8b"},
    json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
print(response.json())
```

## Errors

- A request error means the API refused the call.
- A job failure means the API accepted the transcription, then it failed. Its `status` is `failed`.

### The error envelope

Every `/v1` request error contains an `error` object with `code`, `message` and `retryable`.

- Handle errors by `code`.
- Display `message` to the user. Its wording may change.
- When `retryable` is `true`, the same request may succeed later.

Some errors add fields next to `retryable`:

| Field | On which errors | Meaning |
|---|---|---|
| `action_url` | `insufficient_minutes`, `no_active_plan`, `topup_needs_user` | Where to buy a plan or a top-up. |
| `minutes_needed`, `minutes_remaining` | `insufficient_minutes` | Minutes the request needs (`null` when it is not known yet) and minutes you have. |
| `reason`, `packs` | `topup_needs_user` | Why the card was not charged, and the packs that were requested. |
| `supported_platforms` | `unsupported_platform` | The platforms you can send: `["youtube", "x", "tiktok", "instagram", "facebook", "bluesky", "twitch", "dailymotion", "pinterest", "linkedin", "snapchat"]`. |
| `missing_chunks` | `upload_incomplete` | Indexes of the chunks that did not arrive. |
| `max_bytes` | `media_too_large` | The largest file size allowed for you, in bytes. |
| `params` | `invalid_request` | For body and query validation errors, an object mapping each field to what is wrong with it. |

### Request errors

| HTTP status | `code` | Retryable | When |
|---|---|---|---|
| 400, 422 | `invalid_request` | No | A parameter or body field is missing or invalid, for example `diarize` together with `source: "captions"`, or `reuse_recent` with `webhook_url`. Also returned when the request body is not valid JSON. |
| 422 | `invalid_url` | No | Use a YouTube, X, TikTok, Instagram, Facebook or Bluesky link, or another platform listed in [Sources and options](https://transcript1.com/docs/sources-and-options). Also returned when `platform` does not match the link. |
| 422 | `unsupported_platform` | No | `platform` is not supported. |
| 422 | `unsupported_media` | No | The uploaded file does not have a supported extension. |
| 422 | `unsupported_translation` | No | The `translate_to` language of a live session is not supported, or live translation is not available. |
| 401 | `unauthorized` | No | The credential is missing, invalid, expired or revoked, or the upload token is invalid or expired. |
| 403 | `insufficient_scope` | No | An OAuth token lacks `transcriptions:read` or `transcriptions:write`. |
| 403 | `email_not_verified` | No | Verify your email address before creating transcriptions. |
| 402 | `insufficient_minutes` | No | Your usable minutes are 0 or fewer than the transcription needs. When [auto top-up](https://transcript1.com/docs/billing-and-limits#auto-top-up) is on, transcript1 tries to top up first. Buy minutes at `action_url`, or let an agent buy them with `POST /v1/topups` or the MCP tool `top_up`. |
| 402 | `no_active_plan` | No | You tried to buy a top-up without an active paid plan. `POST /v1/topups` and the dashboard's billing endpoints return this, not the transcription endpoints. |
| 402 | `topup_needs_user` | Only for `pending` and `in_progress` | `POST /v1/topups` did not charge the card. `reason` says why: `not_allowed` (the credential cannot buy top-ups, or its user is not a team admin), `cap_reached` (the monthly agent limit has no room), `card_declined`, `authentication_required` or `no_card`. The user can buy the packs at `action_url`. For `pending` (the payment is not confirmed yet) and `in_progress` (another top-up is running), the charge may still complete: retry later with the same `Idempotency-Key` instead of sending the user to buy. |
| 404 | `not_found` | No | The transcription does not exist or belongs to another team. |
| 409 | `upload_incomplete` | No | Some chunks are missing. Send them and complete again. |
| 409 | `invalid_state` | No | The transcription is not awaiting an upload, so chunks can no longer be sent or completed. |
| 409 | `idempotency_conflict` | No | The `Idempotency-Key` was already used with different parameters. |
| 409 | `session_ended` | No | A live session has ended, so `POST /v1/live/sessions/:id/token` cannot issue a token. Also sent as a WebSocket `error` event. |
| 413 | `media_too_large` | No | The file is larger than your limit. See [Uploads](https://transcript1.com/docs/uploads). |
| 413 | `invalid_request` | No | A JSON body over 1 MB. Files use `media_too_large` instead. |
| 429 | `rate_limited` | Yes | Too many requests per minute. Wait for `Retry-After` seconds. |
| 429 | `too_many_active_jobs` | Yes | Too many transcriptions running, or too many open uploads. Wait for `Retry-After` seconds. |
| 429 | `too_many_live_sessions` | Yes | Your plan's simultaneous live sessions are in use. Wait for `Retry-After` seconds (30). |
| 500 | `internal_error` | Yes | Something went wrong on our side. Retry shortly. |
| 503 | `service_unavailable` | Yes | transcript1 is temporarily unavailable. Retry shortly. |
| 503 | `live_capacity` | Yes | All live capacity is in use. Wait for `Retry-After` seconds (30) and create the session again. |
| 503 | `live_unavailable` | No | Live transcription is not available right now. |

Browser requests authenticated with a login cookie also have their origin checked. A request from another site returns `403 forbidden_origin`.

API calls with an `Authorization` header are not affected.

Every `429` response includes a `Retry-After` header. Its value is the number of seconds to wait. See [Billing and limits](https://transcript1.com/docs/billing-and-limits) for the limits.

### Retry a request

- Retry only when `retryable` is `true`: `rate_limited`, `too_many_active_jobs`, `internal_error`, `service_unavailable`, and `topup_needs_user` with `reason` `pending` or `in_progress`.
- Follow `Retry-After` when present. Otherwise, increase the wait between attempts.
- When retrying `POST /v1/transcriptions` or `POST /v1/topups`, send the same `Idempotency-Key` header. This avoids a second charge if the original request succeeded.
- For every other code, fix the request before sending it again.

### Job failures

A transcription with `status` set to `failed` includes an `error` object with `code` and `message`.

Failures can happen after the request returns. Read `error` when you poll or receive a `transcription.failed` [webhook](https://transcript1.com/docs/api-reference#webhooks). A failed translation sends a `transcription.translation.failed` webhook and never fails the transcript.

| `error.code` | Meaning | Try again? |
|---|---|---|
| `media_unavailable` | The video does not exist or is no longer available. | No. Check the URL. |
| `media_private` | The video is private. | No. Use a public post or a public or unlisted YouTube video. |
| `login_required` | The platform requires a logged-in account. | No. Permanent failure; not retried. |
| `no_audio` | The video has no sound track. | No. Permanent failure; not retried. |
| `age_restricted` | The video is age-restricted. | No. Upload the file instead. |
| `live_stream` | The video is a live stream, or a stream that has not started yet. | After the stream has ended. |
| `no_captions` | `source` was `captions` and the video has no usable caption track. | Use `source: "auto"` or `"stt"`. |
| `media_too_long` | Speech-to-text accepts up to 5 hours of media. | No. Split the media. |
| `unsupported_media` | The file could not be read or has no audio track. | No. Send a supported file. |
| `no_speech` | No speech was detected. | No. |
| `unsupported_language` | The language of this media is not supported. | No. Set `language` to a supported code or `"auto"`. |
| `insufficient_minutes` | Your minutes ran out before the transcription could be charged, and auto top-up, if on, could not add more. | After you top up. |
| `fetch_failed` | Fetching the media failed after several attempts. | Yes, later. |
| `transcription_failed` | Speech-to-text failed after several attempts. | Yes, later. |

Failed and canceled transcriptions are never charged. Any minutes already charged are refunded when the transcription fails or is canceled.

Example: 402 Payment Required

```json
{
  "error": {
    "code": "insufficient_minutes",
    "message": "Not enough minutes",
    "retryable": false,
    "action_url": "https://transcript1.com/dashboard/billing",
    "minutes_needed": 20,
    "minutes_remaining": 3
  }
}
```

## The Transcription object

### The Transcription object

#### Attributes

- `group_id` (string, required, nullable): Public transcription group ID, or null for legacy ungrouped work.
- `id` (string, required)
- `object` (string, required, one of `transcription`)
- `status` (string, required, one of `awaiting_upload`, `queued`, `fetching`, `transcribing`, `completed`, `failed`, `canceled`, `live`)
- `source` (object, required)
  - With `url`
    - `platform` (string, required)
    - `url` (string, required, nullable)
    - `media_id` (string, required, nullable)
  - With `filename`
    - `platform` (string, required, one of `upload`)
    - `filename` (string, required, nullable)
    - `size` (number, required, nullable)
  - With `session_id`
    - `platform` (string, required, one of `live`)
    - `session_id` (string, required, nullable)
- `media` (object, required, nullable)
  - `title` (string, optional, nullable)
  - `author` (string, optional, nullable)
  - `author_url` (string, optional, nullable)
  - `thumbnail_url` (string, optional, nullable)
  - `filename` (string, optional)
  - `mime_type` (string, optional)
  - `size` (number, optional)
  - `duration_seconds` (number, optional, nullable)
- `options` (object, required)
  - `clip` (object, optional, nullable)
    - `start` (number, required)
    - `end` (number, required)
    - `requested_end` (number, optional)
  - `chunk` (object, optional)
    - `audio_start` (number, required)
  - `source` (string, required, one of `auto`, `captions`, `stt`)
  - `language` (string, required)
  - `diarize` (boolean, required)
  - `translate_to` (string, optional)
- `transcript_source` (string, required, nullable, one of `youtube_captions`, `x_captions`, `tiktok_captions`, `bluesky_captions`, `dailymotion_captions`, `linkedin_captions`, `speech_to_text`, `live`)
- `caption_kind` (string, required, nullable, one of `manual`, `auto_generated`): Null until resolved, for speech-to-text, or when native caption metadata does not establish authorship. Unknown does not mean manual.
- `language` (string, required, nullable)
- `billed_minutes` (integer, required, nullable)
- `cached` (boolean, required)
- `error` (object, required, nullable)
  - `code` (string, required)
  - `message` (string, required, nullable)
- `created_at` (string, date-time, required, nullable)
- `completed_at` (string, date-time, required, nullable)
- `deleted_at` (string, date-time, required, nullable): When the transcription was deleted. Always `null` unless the request includes `include_deleted=true`.
- `translation` (object, optional, nullable): The translation this read applies: the requested `translation_language`, else the `translate_to` language, else the most recent one, or `null`. Included in `GET` responses.
  - `language` (string, required)
  - `status` (string, required, one of `processing`, `completed`, `failed`)
  - `error` (string, optional)
  - `same_as_source` (boolean, optional): `true` on a completed translation into the language the transcript is already in. Nothing was translated: its text is the original text, and text, SRT and WebVTT show each line once.
- `translations` (array of objects, optional): Every translation of the transcription, oldest first, including a pending `translate_to` language. Included in `GET` and list responses.
  - `language` (string, required)
  - `status` (string, required, one of `processing`, `completed`, `failed`)
  - `error` (string, optional)
  - `same_as_source` (boolean, optional): `true` on a completed translation into the language the transcript is already in. Nothing was translated: its text is the original text, and text, SRT and WebVTT show each line once.
- `transcript` (object, optional): Included in `GET` responses for completed transcriptions.
  - `text` (string, required)
  - `speakers` (array of strings, required)
  - `segments` (array of objects, required)
    - `start` (number, required)
    - `end` (number, required)
    - `speaker` (string, required, nullable)
    - `language` (string, required, nullable)
    - `text` (string, required)
    - `translation` (string, optional): Translated text, present with `translation=both` once a translation is completed.
    - `words` (array of objects, optional, nullable)
      - `start` (number, required)
      - `end` (number, required)
      - `text` (string, required)
      - `confidence` (number, optional, nullable)
      - `speaker` (string, required, nullable)

#### Example

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": {
    "title": "Example video",
    "author": "Example channel",
    "author_url": "https://www.youtube.com/@examplechannel",
    "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
    "duration_seconds": 212
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "youtube_captions",
  "caption_kind": "auto_generated",
  "language": "en",
  "billed_minutes": 4,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Welcome back to the channel. Today we are looking at how transcripts are made.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 4.2,
        "speaker": null,
        "language": "en",
        "text": "Welcome back to the channel."
      },
      {
        "start": 4.2,
        "end": 9.8,
        "speaker": null,
        "language": "en",
        "text": "Today we are looking at how transcripts are made."
      }
    ]
  }
}
```

## Transcriptions

Create, read, list and delete transcriptions of video links and uploaded files.

### Create a transcription

`POST /v1/transcriptions`

Transcribes an uploaded file or a YouTube, X, TikTok, Instagram, Facebook, Bluesky, Twitch, Dailymotion, Pinterest, LinkedIn, Snapchat link. Social posts must be public; only the first video is used.

Twitch accepts clips and completed VODs; Pinterest accepts native video pins; LinkedIn accepts public video posts; Snapchat accepts Spotlight. New platforms require public recorded native media without supplied account cookies; no live streams or external embeds.

`platform` is optional and detected from the link. Captions are available on YouTube, X, TikTok, Bluesky, Dailymotion and LinkedIn when a usable native track exists. Dailymotion supports direct SRT, not caption playlists; Bluesky captions require a supported trusted PDS; LinkedIn supports public native posts, not Learning. Other platforms use speech-to-text. Caption kind is null when authorship is unknown. All sources use the same per-minute price.

Returns `200` for a final result, `201` when awaiting an upload and `202` while processing. Pass `wait` to wait up to 60 seconds for a final status.

Pass `reuse_recent=true` with a platform URL to reuse a transcription of the same video and options from the last 24 hours. The response includes `reused: true`, with no additional charge. Short links may not be matched. An `Idempotency-Key` replay takes precedence.

#### Query

- `wait` (integer, optional, nullable, default 0): Seconds to wait for a final status before responding, up to 60. Defaults to 0.
- `reuse_recent` (string, optional, default false, one of `true`, `false`): For platform URLs only. Cannot be combined with `webhook_url`. Returns the team's most recent transcription of the same video with the same `source`, `language` and `diarize`. It must have been created in the last 24 hours and must not be `failed` or `canceled`. Reuses that transcription without creating or charging for a new one.

#### Headers

- `Idempotency-Key` (string, optional): Returns the original transcription when you repeat a request with the same key and parameters.

#### Body

- With `url`
  - `platform` (string, optional, one of `youtube`, `x`, `tiktok`, `instagram`, `facebook`, `bluesky`, `twitch`, `dailymotion`, `pinterest`, `linkedin`, `snapchat`): Optional; detected from the link. If supplied, must match the link.
  - `url` (string, required): YouTube, X, TikTok, Instagram, Facebook, Bluesky, Twitch, Dailymotion, Pinterest, LinkedIn, Snapchat video link. Social posts must be public; only the first video is used. New platforms support public recorded native video without supplied account cookies, not live streams or external embeds.
  - `source` (string, optional, one of `auto`, `captions`, `stt`): "auto" uses native captions when available, otherwise speech-to-text. "captions" never falls back to speech-to-text and cannot be combined with diarize or uploads. "stt" always uses speech-to-text; diarize with auto skips captions. Captions are available on YouTube, X, TikTok, Bluesky, Dailymotion and LinkedIn when a usable native track exists. Dailymotion supports direct SRT, not caption playlists; Bluesky captions require a supported trusted PDS; LinkedIn supports public native posts, not Learning. Other platforms use speech-to-text. Caption kind is null when authorship is unknown.
  - `language` (string, optional)
  - `diarize` (boolean, optional)
  - `translate_to` (string, optional): Two-letter language code. Translates the transcript once it completes. Translation is free.
  - `webhook_url` (string, uri, optional)
- With `upload`
  - `upload` (object, required)
    - `filename` (string, required)
    - `size` (integer, required)
    - `mime_type` (string, optional)
  - `source` (string, optional, one of `auto`, `stt`)
  - `language` (string, optional)
  - `diarize` (boolean, optional)
  - `translate_to` (string, optional): Two-letter language code. Translates the transcript once it completes. Translation is free.
  - `webhook_url` (string, uri, optional)
- multipart/form-data
  - `file` (string, binary, required)
  - `source` (string, optional, one of `auto`, `stt`)
  - `language` (string, optional)
  - `diarize` (boolean, optional)
  - `translate_to` (string, optional): Two-letter language code. Translates the transcript once it completes. Translation is free.
  - `webhook_url` (string, uri, optional)

#### Returns

- 200 OK: The transcription reached a final status
- 201 Created: The transcription is awaiting a chunked upload. `upload` describes where to send each chunk.
- 202 Accepted: The transcription is processing
- 400 Bad Request: The JSON request body is malformed
- 401 Unauthorized: Missing or invalid credential
- 402 Payment Required: Not enough minutes. Applies to every request body format.
- 403 Forbidden: Insufficient scope, unverified email, cross-site request or a single-member team plan
- 409 Conflict: `Idempotency-Key` was reused with different parameters.
- 413 Content Too Large: The upload or the JSON request body is too large
- 422 Unprocessable Content: Invalid request, URL, platform or file type
- 429 Too Many Requests: Too many requests or too many active transcriptions. Wait for the number of seconds in `Retry-After`.

```bash
curl -X POST https://api.transcript1.com/v1/transcriptions \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const transcription = await t1.transcriptions.create('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/transcriptions",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
    json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
print(response.json())
```

Response 202 Accepted:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "queued",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": null,
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": null,
  "deleted_at": null,
  "reused": false
}
```

Response 200 OK:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": {
    "title": "Example video",
    "author": "Example channel",
    "author_url": "https://www.youtube.com/@examplechannel",
    "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
    "duration_seconds": 212
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "youtube_captions",
  "caption_kind": "auto_generated",
  "language": "en",
  "billed_minutes": 4,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Welcome back to the channel. Today we are looking at how transcripts are made.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 4.2,
        "speaker": null,
        "language": "en",
        "text": "Welcome back to the channel."
      },
      {
        "start": 4.2,
        "end": 9.8,
        "speaker": null,
        "language": "en",
        "text": "Today we are looking at how transcripts are made."
      }
    ]
  },
  "reused": false
}
```

Response 201 Created:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "awaiting_upload",
  "source": {
    "platform": "upload",
    "filename": "interview.m4a",
    "size": 48210334
  },
  "media": {
    "filename": "interview.m4a",
    "mime_type": "audio/mp4",
    "size": 48210334
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": null,
  "deleted_at": null,
  "reused": false,
  "upload": {
    "chunk_url_template": "https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX/upload/chunks/{index}",
    "headers": {
      "X-Upload-Token": "t1u_Zk3vYb8Nw1ZrT5xLpD0aHcE7sUj9GfMiO2yXtVnR4Bw"
    },
    "expires_at": "2026-10-13T10:30:00.000Z",
    "chunks": [
      {
        "index": 0,
        "offset": 0,
        "size": 838860
      },
      {
        "index": 1,
        "offset": 838860,
        "size": 838860
      },
      {
        "index": 2,
        "offset": 1677720,
        "size": 838860
      },
      {
        "index": 3,
        "offset": 2516580,
        "size": 838860
      },
      {
        "index": 4,
        "offset": 3355440,
        "size": 838860
      },
      {
        "index": 5,
        "offset": 4194300,
        "size": 8388608
      },
      {
        "index": 6,
        "offset": 12582908,
        "size": 8388608
      },
      {
        "index": 7,
        "offset": 20971516,
        "size": 8388608
      },
      {
        "index": 8,
        "offset": 29360124,
        "size": 8388608
      },
      {
        "index": 9,
        "offset": 37748732,
        "size": 8388608
      },
      {
        "index": 10,
        "offset": 46137340,
        "size": 2072994
      }
    ]
  }
}
```

Response 400 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request body is not valid JSON",
    "retryable": false
  }
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 402 · insufficient_minutes:

```json
{
  "error": {
    "code": "insufficient_minutes",
    "message": "Not enough minutes",
    "retryable": false,
    "action_url": "https://transcript1.com/dashboard/billing",
    "minutes_needed": 12,
    "minutes_remaining": 3
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · email_not_verified:

```json
{
  "error": {
    "code": "email_not_verified",
    "message": "Verify your email address to create transcriptions",
    "retryable": false
  }
}
```

Response 403 · forbidden_origin:

```json
{
  "error": {
    "code": "forbidden_origin",
    "message": "Cross-site request rejected",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 409 · idempotency_conflict:

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key was used with different parameters",
    "retryable": false
  }
}
```

Response 413 · media_too_large:

```json
{
  "error": {
    "code": "media_too_large",
    "message": "File exceeds the maximum size",
    "retryable": false,
    "max_bytes": 95000000
  }
}
```

Response 413 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request body is too large (max 1 MB)",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "retryable": false,
    "params": {
      "wait": "Number must be less than or equal to 60"
    }
  }
}
```

Response 422 · invalid_url:

```json
{
  "error": {
    "code": "invalid_url",
    "message": "Not a supported link. Use a YouTube, X, TikTok, Instagram, Facebook, Bluesky, Twitch, Dailymotion, Pinterest, LinkedIn, Snapchat link.",
    "retryable": false
  }
}
```

Response 422 · unsupported_platform:

```json
{
  "error": {
    "code": "unsupported_platform",
    "message": "Platform \"vimeo\" is not supported",
    "retryable": false,
    "supported_platforms": [
      "youtube",
      "x",
      "tiktok",
      "instagram",
      "facebook",
      "bluesky",
      "twitch",
      "dailymotion",
      "pinterest",
      "linkedin",
      "snapchat"
    ]
  }
}
```

Response 422 · unsupported_media:

```json
{
  "error": {
    "code": "unsupported_media",
    "message": "Unsupported file type",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

Response 429 · too_many_active_jobs:

```json
{
  "error": {
    "code": "too_many_active_jobs",
    "message": "Too many active transcriptions (limit 2)",
    "retryable": true
  }
}
```

### Get a transcription

`GET /v1/transcriptions/{id}`

Returns the transcription object. Completed transcriptions include the transcript. Use `format` to render it as text, SRT or WebVTT.

If you request a rendered format before completion, the response is JSON. It returns `202` while processing or `200` if the transcription failed or was canceled.

With `include_deleted=true`, a deleted transcription is returned as a record without a transcript. For deleted records, `wait` is ignored and rendered formats return `404`.

#### Path

- `id` (string, required)

#### Query

- `wait` (integer, optional, nullable, default 0): Seconds to wait for a final status before responding, up to 60. Defaults to 0.
- `wait_for` (string, optional, default translation, one of `translation`, `transcript`): What `wait` waits for. `translation` also waits for a requested translation to finish, unless `translation=none`. `transcript` returns as soon as the transcription itself finishes: a completed transcript may then come with `translation.status` `processing` and no translated text yet; text, SRT and WebVTT then show only the original. Defaults to `translation`.
- `format` (string, optional, default json, one of `json`, `text`, `srt`, `vtt`)
- `include` (string, optional, one of `words`)
- `translation` (string, optional, default both, one of `both`, `only`, `none`): How to include a completed translation. `both` adds a `translation` field to each segment, or a second line in text, SRT and WebVTT. `only` replaces the text with the translation. `none` ignores it. Without a completed translation this has no effect. Defaults to `both`.
- `translation_language` (string, optional): Two-letter code of the translation to read. Defaults to the `translate_to` language, then the most recently created translation. Unknown languages return `404 translation_not_found`.
- `timestamps` (string, optional, default false, one of `true`, `false`)
- `include_deleted` (string, optional, default false, one of `true`, `false`): Also returns deleted transcriptions as read-only records without a transcript. Their `deleted_at` is set. Defaults to `false`.

#### Returns

- 200 OK: The transcription, or the rendered transcript
- 202 Accepted: A rendered format was requested while the transcription is still processing
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope or a single-member team plan
- 404 Not Found: Transcription not found
- 422 Unprocessable Content: Invalid query parameters
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const transcription = await t1.transcriptions.get('tr_4fHq8ZbN2xLm0PqRsTuVwX');
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 · Completed:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": {
    "title": "Example video",
    "author": "Example channel",
    "author_url": "https://www.youtube.com/@examplechannel",
    "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
    "duration_seconds": 212
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "youtube_captions",
  "caption_kind": "auto_generated",
  "language": "en",
  "billed_minutes": 4,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Welcome back to the channel. Today we are looking at how transcripts are made.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 4.2,
        "speaker": null,
        "language": "en",
        "text": "Welcome back to the channel."
      },
      {
        "start": 4.2,
        "end": 9.8,
        "speaker": null,
        "language": "en",
        "text": "Today we are looking at how transcripts are made."
      }
    ]
  }
}
```

Response 200 · tiktok captions (unknown authorship):

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "tiktok",
    "url": "https://www.tiktok.com/@example/video/6718335390845095173",
    "media_id": "6718335390845095173"
  },
  "media": {
    "title": "Example native video",
    "duration_seconds": 10
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "tiktok_captions",
  "caption_kind": null,
  "language": "en",
  "billed_minutes": 1,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Example native captions.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 3,
        "speaker": null,
        "language": "en",
        "text": "Example native captions.",
        "words": null
      }
    ]
  }
}
```

Response 200 · bluesky captions (unknown authorship):

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "bluesky",
    "url": "https://bsky.app/profile/example.bsky.social/post/3lbpqwzq2bk2j",
    "media_id": "example.bsky.social/3lbpqwzq2bk2j"
  },
  "media": {
    "title": "Example native video",
    "duration_seconds": 10
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "bluesky_captions",
  "caption_kind": null,
  "language": "en",
  "billed_minutes": 1,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Example native captions.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 3,
        "speaker": null,
        "language": "en",
        "text": "Example native captions.",
        "words": null
      }
    ]
  }
}
```

Response 200 · dailymotion captions (unknown authorship):

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "dailymotion",
    "url": "https://www.dailymotion.com/video/x123",
    "media_id": "x123"
  },
  "media": {
    "title": "Example native video",
    "duration_seconds": 10
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "dailymotion_captions",
  "caption_kind": null,
  "language": "en",
  "billed_minutes": 1,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Example native captions.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 3,
        "speaker": null,
        "language": "en",
        "text": "Example native captions.",
        "words": null
      }
    ]
  }
}
```

Response 200 · linkedin captions (unknown authorship):

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "completed",
  "source": {
    "platform": "linkedin",
    "url": "https://www.linkedin.com/feed/update/urn:li:activity:123/",
    "media_id": "123"
  },
  "media": {
    "title": "Example native video",
    "duration_seconds": 10
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": "linkedin_captions",
  "caption_kind": null,
  "language": "en",
  "billed_minutes": 1,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:04.000Z",
  "deleted_at": null,
  "transcript": {
    "text": "Example native captions.",
    "speakers": [],
    "segments": [
      {
        "start": 0,
        "end": 3,
        "speaker": null,
        "language": "en",
        "text": "Example native captions.",
        "words": null
      }
    ]
  }
}
```

Response 200 · Failed:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "failed",
  "source": {
    "platform": "instagram",
    "url": "https://www.instagram.com/reel/C8xYz12abCd/",
    "media_id": "C8xYz12abCd"
  },
  "media": null,
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": {
    "code": "media_private",
    "message": "Private posts are not supported"
  },
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": "2026-10-13T09:30:03.000Z",
  "deleted_at": null
}
```

Response 202 Accepted:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "queued",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": null,
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": null,
  "deleted_at": null
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Transcription not found",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "retryable": false,
    "params": {
      "wait": "Number must be less than or equal to 60"
    }
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### List transcriptions

`GET /v1/transcriptions`

Lists your team's transcriptions, newest first, without transcripts. Pass `next_cursor` as `cursor` to get the next page.

Deleted transcriptions are hidden unless you pass `include_deleted=true`.

#### Query

- `limit` (integer, optional, default 20)
- `cursor` (string, optional)
- `status` (string, optional, one of `awaiting_upload`, `queued`, `fetching`, `transcribing`, `completed`, `failed`, `canceled`, `live`)
- `platform` (string, optional)
- `include_deleted` (string, optional, default false, one of `true`, `false`): Also returns deleted transcriptions as read-only records without a transcript. Their `deleted_at` is set. Defaults to `false`.

#### Returns

- 200 OK: A page of transcriptions
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope or a single-member team plan
- 422 Unprocessable Content: Invalid query parameters or cursor
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

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

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
for await (const transcription of t1.transcriptions.list({ limit: 20 })) {
  console.log(transcription.id, transcription.status);
}
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/transcriptions",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "object": "list",
  "data": [
    {
      "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
      "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
      "object": "transcription",
      "status": "completed",
      "source": {
        "platform": "youtube",
        "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        "media_id": "dQw4w9WgXcQ"
      },
      "media": {
        "title": "Example video",
        "author": "Example channel",
        "author_url": "https://www.youtube.com/@examplechannel",
        "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
        "duration_seconds": 212
      },
      "options": {
        "source": "auto",
        "language": "auto",
        "diarize": false
      },
      "transcript_source": "youtube_captions",
      "caption_kind": "auto_generated",
      "language": "en",
      "billed_minutes": 4,
      "cached": false,
      "error": null,
      "created_at": "2026-10-13T09:30:00.000Z",
      "completed_at": "2026-10-13T09:30:04.000Z",
      "deleted_at": null
    }
  ],
  "next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTEwLTEzVDA5OjMwOjAwLjAwMFoiLCJpZCI6IjViMGU2YzFjLTBkNmYtNGMxZi05YjVlLThmM2Y0ZDBhN2ExMSJ9"
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Invalid cursor",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Cancel or delete a transcription

`DELETE /v1/transcriptions/{id}`

Cancels an active transcription or one awaiting an upload. Any charged minutes are refunded.

Deletes a completed, failed or canceled transcription together with its stored transcript.

A live transcription cannot be deleted until its live session has ended; before that the request returns `409 invalid_state`.

#### Path

- `id` (string, required)

#### Returns

- 204 No Content: Canceled or deleted
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope, cross-site request or a single-member team plan
- 404 Not Found: Transcription not found
- 409 Conflict: The transcription belongs to a live session that has not ended
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl -X DELETE https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
await t1.transcriptions.delete('tr_4fHq8ZbN2xLm0PqRsTuVwX');
```

```python
import os
import requests

response = requests.delete(
    "https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
response.raise_for_status()
```

Response 204 No Content:

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · forbidden_origin:

```json
{
  "error": {
    "code": "forbidden_origin",
    "message": "Cross-site request rejected",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Transcription not found",
    "retryable": false
  }
}
```

Response 409 · invalid_state:

```json
{
  "error": {
    "code": "invalid_state",
    "message": "End the live session first",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Transcribe a buffered part of a video

`POST /v1/transcriptions/chunks`

Uploads up to 8 MiB of audio for a 1–120 second window. Times are absolute media seconds. Payment settles actual completed coverage; admission does not reserve balance.

#### Headers

- `Idempotency-Key` (string, optional)

#### Body

- `audio` (string, binary, required)
- `url` (string, required)
- `key` (string, optional)
- `title` (string, optional)
- `duration` (string, optional)
- `start` (string, required)
- `end` (string, required)
- `audio_start` (string, required)
- `language` (string, optional)
- `translate_to` (string, optional)

#### Returns

- 200 OK: Idempotent replay
- 201 Created: Created, queued or completed
- 402 Payment Required: Insufficient minutes
- 409 Conflict: Idempotency conflict
- 413 Content Too Large: Audio exceeds 8 MiB
- 422 Unprocessable Content: Invalid media or window
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

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

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/transcriptions/chunks', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
  },
});
console.log(await response.json());
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/transcriptions/chunks",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "queued",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": null,
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": null,
  "deleted_at": null
}
```

Response 201 Created:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "queued",
  "source": {
    "platform": "youtube",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "media_id": "dQw4w9WgXcQ"
  },
  "media": null,
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": null,
  "deleted_at": null
}
```

Response 402 · insufficient_minutes:

```json
{
  "error": {
    "code": "insufficient_minutes",
    "message": "Not enough minutes",
    "retryable": false
  }
}
```

Response 409 · idempotency_conflict:

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key was used with different parameters",
    "retryable": false
  }
}
```

Response 413 · media_too_large:

```json
{
  "error": {
    "code": "media_too_large",
    "message": "File exceeds the maximum size",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Invalid chunk window",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

## Uploads

Send a large file in chunks. Start with Create a transcription using an `upload` body. Send each chunk, then complete the upload.

### Upload one chunk

`PUT /v1/transcriptions/{id}/upload/chunks/{index}`

Sends one chunk of a chunked upload as `application/octet-stream`.

Authenticate with the `X-Upload-Token` from the create response. This endpoint does not use a bearer credential.

The body must match the planned chunk size exactly. Sending a chunk again replaces it.

#### Path

- `id` (string, required)
- `index` (integer, optional, nullable): Zero-based chunk number from the upload plan

#### Headers

- `X-Upload-Token` (string, required)

#### Body

- `(raw body)` (string, binary, required)

#### Returns

- 200 OK: The chunk was stored
- 401 Unauthorized: Missing, invalid or expired upload token
- 404 Not Found: Transcription not found
- 409 Conflict: The transcription is no longer awaiting an upload
- 422 Unprocessable Content: Index outside the chunk plan or wrong body size

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

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
// the SDK uploads the file in chunks and completes the upload
const transcription = await t1.transcriptions.create('./interview.m4a');
```

```python
import requests

with open("chunk-0.bin", "rb") as f:
    response = requests.put(
        "https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX/upload/chunks/0",
        headers={"X-Upload-Token": "t1u_Zk3vYb8Nw1ZrT5xLpD0aHcE7sUj9GfMiO2yXtVnR4Bw", "Content-Type": "application/octet-stream"},
        data=f,
    )
print(response.json())
```

Response 200 OK:

```json
{
  "object": "upload_chunk",
  "index": 0,
  "size": 838860
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or expired upload token",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Transcription not found",
    "retryable": false
  }
}
```

Response 409 · invalid_state:

```json
{
  "error": {
    "code": "invalid_state",
    "message": "Transcription is not awaiting an upload",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Chunk 0 must be exactly 838860 bytes",
    "retryable": false
  }
}
```

### Get upload progress

`GET /v1/transcriptions/{id}/upload`

Lists the received chunks of a chunked upload so an interrupted upload can resume.

#### Path

- `id` (string, required)

#### Returns

- 200 OK: Upload progress
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope or a single-member team plan
- 404 Not Found: Transcription or upload not found
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

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

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
// the SDK uploads the file in chunks and completes the upload
const transcription = await t1.transcriptions.create('./interview.m4a');
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX/upload",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "object": "upload",
  "status": "awaiting_upload",
  "total_bytes": 48210334,
  "uploaded_bytes": 4194300,
  "received_chunks": [
    0,
    1,
    2,
    3,
    4
  ],
  "chunks": [
    {
      "index": 0,
      "offset": 0,
      "size": 838860
    },
    {
      "index": 1,
      "offset": 838860,
      "size": 838860
    },
    {
      "index": 2,
      "offset": 1677720,
      "size": 838860
    },
    {
      "index": 3,
      "offset": 2516580,
      "size": 838860
    },
    {
      "index": 4,
      "offset": 3355440,
      "size": 838860
    },
    {
      "index": 5,
      "offset": 4194300,
      "size": 8388608
    },
    {
      "index": 6,
      "offset": 12582908,
      "size": 8388608
    },
    {
      "index": 7,
      "offset": 20971516,
      "size": 8388608
    },
    {
      "index": 8,
      "offset": 29360124,
      "size": 8388608
    },
    {
      "index": 9,
      "offset": 37748732,
      "size": 8388608
    },
    {
      "index": 10,
      "offset": 46137340,
      "size": 2072994
    }
  ],
  "expires_at": "2026-10-13T10:30:00.000Z"
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Transcription not found",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Complete a chunked upload

`POST /v1/transcriptions/{id}/upload/complete`

Checks that every planned chunk arrived with the planned size. Then assembles the file and queues the transcription.

#### Path

- `id` (string, required)

#### Returns

- 202 Accepted: The transcription is queued
- 401 Unauthorized: Missing or invalid credential
- 402 Payment Required: Not enough minutes to queue the transcription. The upload stays awaiting completion.
- 403 Forbidden: Insufficient scope, cross-site request or a single-member team plan
- 404 Not Found: Transcription or upload not found
- 409 Conflict: `upload_incomplete` means chunks are missing. `missing_chunks` lists them. `invalid_state` means the transcription is not awaiting an upload.
- 429 Too Many Requests: Too many requests or too many active transcriptions. Wait for the number of seconds in `Retry-After`.

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

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
// the SDK uploads the file in chunks and completes the upload
const transcription = await t1.transcriptions.create('./interview.m4a');
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/transcriptions/tr_4fHq8ZbN2xLm0PqRsTuVwX/upload/complete",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 202 Accepted:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "object": "transcription",
  "status": "queued",
  "source": {
    "platform": "upload",
    "filename": "interview.m4a",
    "size": 48210334
  },
  "media": {
    "filename": "interview.m4a",
    "mime_type": "audio/mp4",
    "size": 48210334
  },
  "options": {
    "source": "auto",
    "language": "auto",
    "diarize": false
  },
  "transcript_source": null,
  "caption_kind": null,
  "language": null,
  "billed_minutes": null,
  "cached": false,
  "error": null,
  "created_at": "2026-10-13T09:30:00.000Z",
  "completed_at": null,
  "deleted_at": null
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 402 · insufficient_minutes:

```json
{
  "error": {
    "code": "insufficient_minutes",
    "message": "Not enough minutes",
    "retryable": false,
    "action_url": "https://transcript1.com/dashboard/billing",
    "minutes_needed": null,
    "minutes_remaining": 0
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · forbidden_origin:

```json
{
  "error": {
    "code": "forbidden_origin",
    "message": "Cross-site request rejected",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Transcription not found",
    "retryable": false
  }
}
```

Response 409 · upload_incomplete:

```json
{
  "error": {
    "code": "upload_incomplete",
    "message": "Missing chunks",
    "retryable": false,
    "missing_chunks": [
      3,
      7
    ]
  }
}
```

Response 409 · invalid_state:

```json
{
  "error": {
    "code": "invalid_state",
    "message": "Transcription is not awaiting an upload",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

Response 429 · too_many_active_jobs:

```json
{
  "error": {
    "code": "too_many_active_jobs",
    "message": "Too many active transcriptions (limit 2)",
    "retryable": true
  }
}
```

## Account

Your plan, remaining minutes, rate limits and top-ups.

### The Account object

#### Attributes

- `object` (string, required, one of `account`)
- `user` (object, required)
  - `id` (string, required)
  - `email` (string, required)
- `plan` (object, required, nullable)
  - `id` (string, required)
  - `name` (string, required)
- `minutes` (object, required)
  - `remaining` (number, required)
  - `cycle` (number, required)
  - `topup` (number, required)
  - `topup_usable` (boolean, required)
  - `resets_at` (string, date-time, required, nullable)
- `renews_at` (string, date-time, required, nullable)
- `limits` (object, required)
  - `requests_per_minute` (number, required)
  - `max_active_jobs` (number, required)
- `rates` (object, required)
  - `batch` (number, required)
  - `live` (number, required)
- `topup` (object, required, nullable): Top-up packs on the current paid plan. `null` without a paid plan.
  - `pack_minutes` (integer, required): Minutes in one top-up pack
  - `pack_price` (integer, required): Price of one pack in the smallest currency unit (cents)
  - `currency` (string, required): Lowercase ISO currency code
  - `can_charge` (boolean, required): Whether `POST /topups` with this credential charges the card on file right away: the credential may buy top-ups, its user is a team admin, a card is on file and the monthly agent limit has room for one pack.
  - `auto` (object, required)
    - `enabled` (boolean, required)
    - `threshold_minutes` (integer, required): Auto top-up buys packs when the balance drops below this
    - `packs` (integer, required): Packs each auto top-up buys
    - `paused_reason` (string, required, nullable, one of `card_declined`, `authentication_required`, `no_card`): Why auto top-up is paused, `null` while it is not. The user resumes it on the billing page.

#### Example

```json
{
  "object": "account",
  "user": {
    "id": "5b0e6c1c-0d6f-4c1f-9b5e-8f3f4d0a7a11",
    "email": "you@example.com"
  },
  "plan": {
    "id": "9d4e0c0a-2b8a-4f0c-8d63-6a8d3c8a1f22",
    "name": "Monthly"
  },
  "minutes": {
    "remaining": 1180,
    "cycle": 980,
    "topup": 200,
    "topup_usable": true,
    "resets_at": "2026-11-13T09:30:00.000Z"
  },
  "renews_at": "2026-11-13T09:30:00.000Z",
  "limits": {
    "requests_per_minute": 200,
    "max_active_jobs": 10
  },
  "rates": {
    "batch": 1,
    "live": 1
  },
  "topup": {
    "pack_minutes": 1000,
    "pack_price": 600,
    "currency": "usd",
    "can_charge": true,
    "auto": {
      "enabled": true,
      "threshold_minutes": 100,
      "packs": 1,
      "paused_reason": null
    }
  }
}
```

### Get your account

`GET /v1/account`

Returns the authenticated user, current plan, remaining transcription minutes and top-up packs. `topup.can_charge` tells whether `POST /topups` with this credential charges right away.

#### Returns

- 200 OK: The authenticated user's account
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope or a single-member team plan
- 404 Not Found: No account found
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/account \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const account = await t1.account();
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/account",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "object": "account",
  "user": {
    "id": "5b0e6c1c-0d6f-4c1f-9b5e-8f3f4d0a7a11",
    "email": "you@example.com"
  },
  "plan": {
    "id": "9d4e0c0a-2b8a-4f0c-8d63-6a8d3c8a1f22",
    "name": "Monthly"
  },
  "minutes": {
    "remaining": 1180,
    "cycle": 980,
    "topup": 200,
    "topup_usable": true,
    "resets_at": "2026-11-13T09:30:00.000Z"
  },
  "renews_at": "2026-11-13T09:30:00.000Z",
  "limits": {
    "requests_per_minute": 200,
    "max_active_jobs": 10
  },
  "rates": {
    "batch": 1,
    "live": 1
  },
  "topup": {
    "pack_minutes": 1000,
    "pack_price": 600,
    "currency": "usd",
    "can_charge": true,
    "auto": {
      "enabled": true,
      "threshold_minutes": 100,
      "packs": 1,
      "paused_reason": null
    }
  }
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "No account found for this credential",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Buy top-up minutes

`POST /v1/topups`

Charges the card on file for top-up packs and adds their minutes to the balance. Pack size and price depend on the plan: read `topup` from `GET /account` first.

Charging needs an API key with "Can buy top-ups" switched on, or an OAuth token with `billing:write`, whose user is a team admin. Agent purchases are limited by the monthly agent limit set on the billing page.

When the card is not charged, `402 topup_needs_user` gives the `reason`. For `pending` and `in_progress` the charge may still complete: retry later with the same `Idempotency-Key`. For the other reasons the user can buy the packs at `action_url`. Repeat a request with the same `Idempotency-Key` to get the original top-up without paying again.

#### Headers

- `Idempotency-Key` (string, optional): Returns the original top-up when you repeat a request with the same key and packs.

#### Body

- `packs` (integer, optional, default 1): Packs to buy. `GET /account` shows the pack size and price.

#### Returns

- 201 Created: The card was charged and the minutes were added
- 400 Bad Request: The JSON request body is malformed
- 401 Unauthorized: Missing or invalid credential
- 402 Payment Required: `topup_needs_user` gives a `reason`. `pending` (Stripe has not confirmed the payment yet) and `in_progress` (another top-up is running) are retryable: the charge may still complete, so do not send the user to buy. Retry later with the same `Idempotency-Key`, or check `GET /account`. For `not_allowed` (the credential cannot buy top-ups, or its user is not a team admin), `cap_reached` (the monthly agent limit has no room), `card_declined`, `authentication_required` and `no_card`, nothing was charged and the user can buy the packs at `action_url`. `no_active_plan` means the team is on the Free plan; the user chooses a plan at `action_url`.
- 403 Forbidden: Insufficient scope, cross-site request or a single-member team plan
- 409 Conflict: `Idempotency-Key` was reused with different packs
- 422 Unprocessable Content: Invalid packs or Idempotency-Key
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl -X POST https://api.transcript1.com/v1/topups \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"packs":2}'
```

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/topups', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
    'Content-Type': 'application/json',
  },
  body: '{"packs":2}',
});
console.log(await response.json());
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/topups",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
    json={"packs": 2},
)
print(response.json())
```

Response 201 Created:

```json
{
  "object": "topup",
  "status": "succeeded",
  "packs": 2,
  "minutes": 2000,
  "amount": 1200,
  "currency": "usd",
  "card": "visa ****4242",
  "minutes_remaining": 2150
}
```

Response 400 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request body is not valid JSON",
    "retryable": false
  }
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 402 · topup_needs_user:

```json
{
  "error": {
    "code": "topup_needs_user",
    "message": "This top-up would go over the monthly agent top-up limit",
    "retryable": false,
    "reason": "cap_reached",
    "packs": 2,
    "action_url": "https://transcript1.com/dashboard/billing?topup=2"
  }
}
```

Response 402 · no_active_plan:

```json
{
  "error": {
    "code": "no_active_plan",
    "message": "Top-ups need an active paid plan",
    "retryable": false,
    "action_url": "https://transcript1.com/dashboard/billing"
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · forbidden_origin:

```json
{
  "error": {
    "code": "forbidden_origin",
    "message": "Cross-site request rejected",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 409 · idempotency_conflict:

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key was used with different packs",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "retryable": false,
    "params": {
      "packs": "Number must be less than or equal to 20"
    }
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

## Live

Transcribe audio as it happens. Create a session, then stream PCM audio over a WebSocket with the single-use token and read subtitle lines back.

### The LiveSession object

#### Attributes

- `id` (string, required)
- `status` (string, required, one of `waiting`, `connected`, `disconnected`, `ended`)
- `end_reason` (string, required, nullable, one of `client_ended`, `balance_exhausted`, `idle_timeout`, `max_duration`, `disconnected`, `upstream_error`, `internal_error`, `not_connected`)
- `transcription_id` (string, required, nullable)
- `options` (object, required)
  - `language` (string, required)
  - `translate_to` (string, required, nullable)
  - `diarize` (boolean, required)
  - `audio_format` (string, required, one of `pcm_s16le`)
  - `sample_rate` (integer, required)
  - `save` (boolean, required)
- `connected_ms` (integer, required)
- `audio_ms` (integer, required)
- `minutes_charged` (integer, required): Balance minutes charged so far, after the rate.
- `rate` (number, required): Balance minutes charged per started minute of the session.
- `created_at` (string, date-time, required, nullable)
- `ended_at` (string, date-time, required, nullable)

#### Example

```json
{
  "id": "lv_7Kd2mQp9XvTn4RbWc1HsYe",
  "status": "connected",
  "end_reason": null,
  "transcription_id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "options": {
    "language": "auto",
    "translate_to": "es",
    "diarize": false,
    "audio_format": "pcm_s16le",
    "sample_rate": 16000,
    "save": true
  },
  "connected_ms": 93400,
  "audio_ms": 91000,
  "minutes_charged": 2,
  "rate": 1,
  "created_at": "2026-11-01T12:00:00.000Z",
  "ended_at": null
}
```

### The LiveCapabilities object

### Create a live session

`POST /v1/live/sessions`

Starts a live transcription session and returns a single-use WebSocket token. Connect with `new WebSocket(websocket_url, ["t1-live", token])` within 60 seconds, then send PCM audio.

Live sessions are billed by connected time: each started minute costs `rate` balance minutes. Creating a session needs enough balance for the first minute. Read `GET /live/capabilities` for the languages, translation targets and limits of the active provider.

#### Body

- `language` (string, optional, default auto): `auto` or a two-letter code the active provider supports. See `GET /live/capabilities`.
- `translate_to` (string, optional): Two-letter code. Must be a valid target (and pair) for the active provider.
- `diarize` (boolean, optional, default false): Label speakers. Rejected where the provider cannot diarize in that mode.
- `audio_format` (string, optional, default pcm_s16le, one of `pcm_s16le`): Mono signed 16-bit little-endian PCM. The only supported format.
- `sample_rate` (integer, optional, default 16000): Sample rate of the audio you send, in Hz.
- `save` (boolean, optional, default true): Save the session as a transcription. `false` keeps no transcript after the session ends.

#### Returns

- 201 Created: The session was created. Connect with the token before it expires.
- 400 Bad Request: The JSON request body is malformed
- 401 Unauthorized: Missing or invalid credential
- 402 Payment Required: The usable balance is below the first minute.
- 403 Forbidden: Insufficient scope, unverified email, cross-site request or a single-member team plan
- 422 Unprocessable Content: Invalid request, language or translation
- 429 Too Many Requests: Too many requests or too many live sessions. Wait for the number of seconds in `Retry-After`.
- 503 Service Unavailable: Live transcription is at capacity (`live_capacity`, retryable) or not configured (`live_unavailable`, not retryable). `Retry-After` is sent for `live_capacity` only.

```bash
curl -X POST https://api.transcript1.com/v1/live/sessions \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"language":"auto","translate_to":"es","diarize":false,"audio_format":"pcm_s16le","sample_rate":16000,"save":true}'
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const live = await t1.live.start({ language: 'auto', sampleRate: 16000 });
live.on('line', line => console.log(line.text));
// the SDK mints the WebSocket token for you. See /docs/live for the raw protocol
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/live/sessions",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
    json={"language": "auto", "translate_to": "es", "diarize": False, "audio_format": "pcm_s16le", "sample_rate": 16000, "save": True},
)
print(response.json())
```

Response 201 Created:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "id": "lv_7Kd2mQp9XvTn4RbWc1HsYe",
  "status": "waiting",
  "transcription_id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "websocket_url": "wss://api.transcript1.com/v1/live/sessions/lv_7Kd2mQp9XvTn4RbWc1HsYe/stream",
  "token": "Q2xhdWRlLXRva2VuLWV4YW1wbGUtMzItYnl0ZXMtYmFzZTY0dXJs",
  "token_expires_at": "2026-11-01T12:01:00.000Z",
  "rate": 1
}
```

Response 400 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request body is not valid JSON",
    "retryable": false
  }
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 402 · insufficient_minutes:

```json
{
  "error": {
    "code": "insufficient_minutes",
    "message": "Not enough minutes",
    "retryable": false,
    "action_url": "https://transcript1.com/dashboard/billing",
    "minutes_needed": 2,
    "minutes_remaining": 1
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · email_not_verified:

```json
{
  "error": {
    "code": "email_not_verified",
    "message": "Verify your email address to create live sessions",
    "retryable": false
  }
}
```

Response 403 · forbidden_origin:

```json
{
  "error": {
    "code": "forbidden_origin",
    "message": "Cross-site request rejected",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 422 · invalid_request:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed",
    "retryable": false,
    "params": {
      "sample_rate": "Number must be greater than or equal to 8000"
    }
  }
}
```

Response 422 · unsupported_language:

```json
{
  "error": {
    "code": "unsupported_language",
    "message": "Language \"xx\" is not supported for live transcription.",
    "retryable": false
  }
}
```

Response 422 · unsupported_translation:

```json
{
  "error": {
    "code": "unsupported_translation",
    "message": "Live translation into \"xx\" is not supported for this language.",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

Response 429 · too_many_live_sessions:

```json
{
  "error": {
    "code": "too_many_live_sessions",
    "message": "Too many live sessions (limit 1)",
    "retryable": true
  }
}
```

Response 503 · live_capacity:

```json
{
  "error": {
    "code": "live_capacity",
    "message": "Live transcription is at capacity. Try again shortly.",
    "retryable": true
  }
}
```

Response 503 · live_unavailable:

```json
{
  "error": {
    "code": "live_unavailable",
    "message": "Live transcription is not available",
    "retryable": false
  }
}
```

### Get a new WebSocket token

`POST /v1/live/sessions/{id}/token`

Returns a new single-use token for a new connection: to resume after a dropped connection, or to take over from a socket the server still thinks is open. Fails with `409 session_ended` once the session has ended.

#### Path

- `id` (string, required)

#### Returns

- 200 OK: A new token for the session
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope, cross-site request or a single-member team plan
- 404 Not Found: Live session not found
- 409 Conflict: The session has ended
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl -X POST https://api.transcript1.com/v1/live/sessions/lv_7Kd2mQp9XvTn4RbWc1HsYe/token \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const live = await t1.live.start({ language: 'auto', sampleRate: 16000 });
live.on('line', line => console.log(line.text));
// the SDK mints the WebSocket token for you. See /docs/live for the raw protocol
```

```python
import os
import requests

response = requests.post(
    "https://api.transcript1.com/v1/live/sessions/lv_7Kd2mQp9XvTn4RbWc1HsYe/token",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "token": "Q2xhdWRlLXRva2VuLWV4YW1wbGUtMzItYnl0ZXMtYmFzZTY0dXJs",
  "token_expires_at": "2026-11-01T12:01:00.000Z",
  "websocket_url": "wss://api.transcript1.com/v1/live/sessions/lv_7Kd2mQp9XvTn4RbWc1HsYe/stream"
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · forbidden_origin:

```json
{
  "error": {
    "code": "forbidden_origin",
    "message": "Cross-site request rejected",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Live session not found",
    "retryable": false
  }
}
```

Response 409 · session_ended:

```json
{
  "error": {
    "code": "session_ended",
    "message": "This live session has ended",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Get a live session

`GET /v1/live/sessions/{id}`

Returns the status, options, connected time and minutes charged of a live session of your team.

#### Path

- `id` (string, required)

#### Returns

- 200 OK: The live session
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope or a single-member team plan
- 404 Not Found: Live session not found
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/live/sessions/lv_7Kd2mQp9XvTn4RbWc1HsYe \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const session = await t1.live.get('lv_7Kd2mQp9XvTn4RbWc1HsYe');
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/live/sessions/lv_7Kd2mQp9XvTn4RbWc1HsYe",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "id": "lv_7Kd2mQp9XvTn4RbWc1HsYe",
  "status": "connected",
  "end_reason": null,
  "transcription_id": "tr_4fHq8ZbN2xLm0PqRsTuVwX",
  "options": {
    "language": "auto",
    "translate_to": "es",
    "diarize": false,
    "audio_format": "pcm_s16le",
    "sample_rate": 16000,
    "save": true
  },
  "connected_ms": 93400,
  "audio_ms": 91000,
  "minutes_charged": 2,
  "rate": 1,
  "created_at": "2026-11-01T12:00:00.000Z",
  "ended_at": null
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 404 · not_found:

```json
{
  "error": {
    "code": "not_found",
    "message": "Live session not found",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Get live capabilities

`GET /v1/live/capabilities`

Describes the active live provider: supported languages, translation targets, diarization, audio format, sample rates and the rate. Returns `{ "available": false, "rate": 1 }` when live transcription is not configured.

#### Returns

- 200 OK: The active provider's capabilities
- 401 Unauthorized: Missing or invalid credential
- 403 Forbidden: Insufficient scope or a single-member team plan
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/live/capabilities \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
import { Transcript1 } from '@transcript1/sdk';

const t1 = new Transcript1();
const capabilities = await t1.live.capabilities();
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/live/capabilities",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 · Available:

```json
{
  "available": true,
  "auto_language": true,
  "languages": [
    "en",
    "es",
    "de",
    "fr"
  ],
  "translation": {
    "targets": [
      "en",
      "es",
      "de",
      "fr"
    ],
    "pairs": "any",
    "requires_language": false
  },
  "diarize": true,
  "audio_formats": [
    "pcm_s16le"
  ],
  "sample_rate": {
    "min": 8000,
    "max": 48000
  },
  "max_session_minutes": 300,
  "rate": 1
}
```

Response 200 · Not configured:

```json
{
  "available": false,
  "rate": 1
}
```

Response 401 · unauthorized:

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid session",
    "retryable": false
  }
}
```

Response 403 · insufficient_scope:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth scope is insufficient",
    "retryable": false
  }
}
```

Response 403 · plan_single_member:

```json
{
  "error": {
    "code": "plan_single_member",
    "message": "This team plan only supports one active member. Contact your team admin to upgrade.",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

## Groups

Group transcriptions by media, read merged coverage, and manage their lifecycle.

### List transcription groups

`GET /v1/groups`

Returns team-scoped groups, newest update first, using cursor pagination.

#### Query

- `limit` (integer, optional, default 20)
- `cursor` (string, optional)

#### Returns

- 200 OK: Successful response
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/groups \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/groups', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
  },
});
console.log(await response.json());
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/groups",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "data": [
    {
      "id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
      "title": "Example video",
      "url": "https://youtu.be/dQw4w9WgXcQ",
      "platform": "youtube",
      "media_id": "dQw4w9WgXcQ",
      "duration_seconds": 600,
      "covered": [
        [
          0,
          60
        ]
      ],
      "covered_seconds": 60,
      "sources": [
        "download"
      ],
      "languages": [
        "en"
      ],
      "translations": [],
      "members_count": 1,
      "active": false,
      "billed_minutes": 1,
      "created_at": "2026-11-08T10:00:00.000Z",
      "updated_at": "2026-11-08T10:00:00.000Z"
    }
  ],
  "next_cursor": null
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Find a group by media URL

`GET /v1/groups/lookup`

Resolves the URL and optional page key within your team.

#### Query

- `url` (string, required)
- `key` (string, optional)

#### Returns

- 200 OK: Successful response
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl "https://api.transcript1.com/v1/groups/lookup?url=%3Curl%3E" \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/groups/lookup?url=%3Curl%3E', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
  },
});
console.log(await response.json());
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/groups/lookup",
    params={"url": "<url>"},
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "title": "Example video",
  "url": "https://youtu.be/dQw4w9WgXcQ",
  "platform": "youtube",
  "media_id": "dQw4w9WgXcQ",
  "duration_seconds": 600,
  "covered": [
    [
      0,
      60
    ]
  ],
  "covered_seconds": 60,
  "sources": [
    "download"
  ],
  "languages": [
    "en"
  ],
  "translations": [],
  "members_count": 1,
  "active": false,
  "billed_minutes": 1,
  "created_at": "2026-11-08T10:00:00.000Z",
  "updated_at": "2026-11-08T10:00:00.000Z"
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Read a group and up to 500 members

`GET /v1/groups/{id}`

Returns metadata and the newest members with a truncation flag.

#### Path

- `id` (string, required)

#### Returns

- 200 OK: Successful response
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
  },
});
console.log(await response.json());
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "title": "Example video",
  "url": "https://youtu.be/dQw4w9WgXcQ",
  "platform": "youtube",
  "media_id": "dQw4w9WgXcQ",
  "duration_seconds": 600,
  "covered": [
    [
      0,
      60
    ]
  ],
  "covered_seconds": 60,
  "sources": [
    "download"
  ],
  "languages": [
    "en"
  ],
  "translations": [],
  "members_count": 1,
  "active": false,
  "billed_minutes": 1,
  "created_at": "2026-11-08T10:00:00.000Z",
  "updated_at": "2026-11-08T10:00:00.000Z",
  "members": [],
  "members_truncated": false
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Cancel active work and delete a group

`DELETE /v1/groups/{id}`

Ends live sessions, cancels active batch work and soft-deletes members. Prior paid coverage is retained.

#### Path

- `id` (string, required)

#### Returns

- 204 No Content: Deleted; settled payments and allocations are retained
- 409 Conflict: New active work joined during deletion; retry
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl -X DELETE https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
  },
});
console.log(response.status);
```

```python
import os
import requests

response = requests.delete(
    "https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
response.raise_for_status()
```

Response 204 No Content:

Response 409 · invalid_state:

```json
{
  "error": {
    "code": "invalid_state",
    "message": "New work joined this group during deletion; retry",
    "retryable": false
  }
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

### Read the merged transcript

`GET /v1/groups/{id}/transcript`

Merges completed members at absolute media timestamps, optionally restricted to a time window.

#### Path

- `id` (string, required)

#### Query

- `from` (number, optional, nullable)
- `to` (number, optional)
- `translation_language` (string, optional)
- `format` (string, optional, default json, one of `json`, `text`, `srt`, `vtt`)

#### Returns

- 200 OK: Merged transcript in the requested format
- 429 Too Many Requests: Too many requests. Wait for the number of seconds in `Retry-After`.

```bash
curl https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX/transcript \
  -H "Authorization: Bearer $TRANSCRIPT1_API_KEY"
```

```javascript
// not in @transcript1/sdk yet. Use the HTTP API.
const response = await fetch('https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX/transcript', {
  method: 'GET',
  headers: {
    Authorization: 'Bearer ' + process.env.TRANSCRIPT1_API_KEY,
  },
});
console.log(await response.json());
```

```python
import os
import requests

response = requests.get(
    "https://api.transcript1.com/v1/groups/tg_4fHq8ZbN2xLm0PqRsTuVwX/transcript",
    headers={"Authorization": f"Bearer {os.environ['TRANSCRIPT1_API_KEY']}"},
)
print(response.json())
```

Response 200 OK:

```json
{
  "group_id": "tg_4fHq8ZbN2xLm0PqRsTuVwX",
  "language": null,
  "covered": [
    [
      0,
      60
    ]
  ],
  "segments": []
}
```

Response 429 · rate_limited:

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests",
    "retryable": true
  }
}
```

## Webhooks

Pass `webhook_url` when you create a transcription. transcript1 sends a signed `POST` to that URL when the transcription completes or fails.

Webhooks are part of the REST API only. The [MCP server](https://transcript1.com/docs/mcp) has no webhook parameter.

### Set a webhook URL

Send `webhook_url` with `POST /v1/transcriptions`:

- For a supported video link or chunked upload reservation, add it to the JSON body.
- For a one-shot upload, use a form field.

The URL must:

- Use HTTPS.
- Contain no user name or password.
- Resolve to a public address.

Private, loopback, link-local and other non-public addresses return `422 invalid_request`. The address is checked when you create the transcription and at every delivery.

`webhook_url` cannot be combined with `reuse_recent=true`.

### Events

| `type` | Sent when |
|---|---|
| `transcription.completed` | The transcription reached `completed`. |
| `transcription.failed` | The transcription reached `failed`. `data.error` has the `code` and `message`. |
| `transcription.translation.completed` | A requested [translation](https://transcript1.com/docs/translation) reached `completed`. |
| `transcription.translation.failed` | A requested translation reached `failed`. |

A canceled transcription sends no webhook. For the two translation events, `data` also has `translation: { language, status, error }`. The error is set only when the translation failed. `same_as_source: true` marks a translation into the language the transcript is already in: nothing was translated, and the event is sent at once. `transcription.completed` still fires when the transcript is done, and translation events are sent separately when the translation finishes. Deleted transcriptions send no translation events.

### Payload

The request body is JSON:

- `created_at` is when the event was created.
- `data` is the transcription object without `transcript`.

To read the transcript, call `GET /v1/transcriptions/:id` with the `id` from `data`.

The request includes these headers:

- `Content-Type: application/json`
- `User-Agent: transcript1-webhooks/1.0`
- `X-Transcript1-Signature`, described below.

### Verify the signature

Every delivery carries the header `X-Transcript1-Signature` with the value `t=<unix seconds>,v1=<hex digest>`.

- `t` is the Unix time in seconds when the request was signed.
- `v1` is the hex-encoded HMAC-SHA256 of `"<t>.<raw body>"`. This string contains the value of `t`, a dot and the request body exactly as received. Use your full signing secret as the key, including the `whsec_` prefix.

Verify the raw body bytes before parsing JSON. Parsing and serializing can change whitespace or key order, invalidating the signature.

- Compare signatures in constant time.
- Reject requests whose `t` is too far from the current time. The examples allow 5 minutes.
- With Express, read the body as raw text so it is not parsed first.
- With Flask, pass `request.get_data(as_text=True)` as `raw_body`.

### Your signing secret

Your team's signing secret starts with `whsec_`. It signs webhooks for every transcription the team creates.

Reveal or rotate it on the [Webhooks page](https://transcript1.com/dashboard/webhooks).

Rotation replaces the secret immediately. All webhooks sent afterwards use the new secret.

### Delivery rules

- Any `2xx` response counts as delivered.
- Any other status code, connection error or no response within the 10-second timeout counts as a failed attempt.
- Failed deliveries are retried with exponential backoff, up to 6 attempts in total. The waits are 2, 4, 8, 16 and 32 minutes. The last attempt is about an hour after the first.
- Use the final URL. transcript1 does not follow redirects, and a `3xx` response counts as a failed attempt.
- Deliveries can arrive more than once. This can happen if your server responds after the timeout. Use `data.id` and `type` to ignore duplicates.

```javascript
import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';

// header: X-Transcript1-Signature, rawBody: the request body exactly as received,
// secret: your full signing secret including the whsec_ prefix.
export const verifySignature = (secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) => {
  const fields = Object.fromEntries(header.split(',').map(part => part.trim().split('=')));
  const t = Number(fields.t);

  if (!Number.isInteger(t) || !fields.v1 || Math.abs(nowSeconds - t) > 300) {
    return false;
  }

  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');

  return fields.v1.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(fields.v1), Buffer.from(expected));
};
```

```python
import hashlib
import hmac
import time


def verify_signature(secret, header, raw_body, now=None):
    # header: the X-Transcript1-Signature header, raw_body: the request body exactly as received,
    # secret: your full signing secret including the whsec_ prefix.
    fields = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        t = int(fields["t"])
    except (KeyError, ValueError):
        return False

    now = int(time.time()) if now is None else now
    if abs(now - t) > 300:
        return False

    expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(fields.get("v1", ""), expected)
```

Example: transcription.completed

```json
{
  "type": "transcription.completed",
  "created_at": "2026-10-13T09:30:04.000Z",
  "data": {
    "id": "tr_8fK2Lm0PqRsTuVwXyZaBcD",
    "object": "transcription",
    "status": "completed",
    "transcript_source": "youtube_captions",
    "billed_minutes": 4,
    "error": null
  }
}
```
