# Dokumentacja API

> Sprawdź endpointy /v1, ich parametry i przykłady wywołań. Specyfikację OpenAPI pobierzesz w JSON lub YAML.

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

## Na początek

Endpointy API znajdziesz pod adresem `https://api.transcript1.com/v1`.

- Wysyłaj body requestów w JSON. W tym samym formacie dostajesz odpowiedzi, z nazwami pól w `snake_case`.
- Daty i godziny zapisujemy w ISO 8601, w UTC, np. `2026-10-13T09:30:00.000Z`.
- Długość nagrania oraz czasy w transkrypcji podajemy w sekundach.
- Błędy mają wspólny format. [Sprawdź, jak je obsługiwać](https://transcript1.com/pl/docs/api-reference#errors).
- W odpowiedzi na uwierzytelniony request dostajesz nagłówki `X-RateLimit-Limit` i `X-RateLimit-Remaining`. Limity opisujemy [na stronie o planach i minutach](https://transcript1.com/pl/docs/billing-and-limits).
- Przy `source=auto` najpierw sprawdzamy napisy w języku nagrania na YouTube, X, TikToku, Bluesky, Dailymotion i LinkedIn. Jeśli nie da się z nich skorzystać, przepisujemy audio. Nie każdy film ma napisy. Tryb `source=captions` (tylko napisy) nie pobiera audio i nie uruchamia STT. Jeśli brakuje odpowiednich napisów, dostaniesz `no_captions`. Ustawienie `source=stt` lub podział na rozmówców od razu uruchamia transkrypcję audio. [W opisie źródeł](https://transcript1.com/pl/docs/sources-and-options) sprawdzisz ograniczenia pobierania oraz dostępność czasów całych fragmentów i poszczególnych słów.

Specyfikację OpenAPI do wykorzystania w narzędziach pobierzesz w jednym z formatów:

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

## Uwierzytelnianie

Klucz API lub token wstaw do nagłówka `Authorization` w formacie `Authorization: Bearer <token>`.

- **Klucz API** rozpoznasz po prefiksie `t1_`. Utwórz go na stronie [„API keys”](https://transcript1.com/dashboard/account/tokens). Daje dostęp do wszystkich endpointów.
- **Token dostępu OAuth** ma prefiks `t1o_`. Dostają go aplikacje MCP, np. Claude i ChatGPT, gdy łączysz je z transcript1. Token ma uprawnienia `transcriptions:read` i `transcriptions:write`.

| Uprawnienie | Dostępne wywołania |
|---|---|
| `transcriptions:read` | `GET /v1/transcriptions`, `GET /v1/transcriptions/:id`, `GET /v1/transcriptions/:id/upload` i `GET /v1/account` |
| `transcriptions:write` | `POST /v1/transcriptions`, `DELETE /v1/transcriptions/:id` i `POST /v1/transcriptions/:id/upload/complete` |

- Jeśli token nie ma wymaganego uprawnienia, API zwróci `403 insufficient_scope`.
- Jeśli nie podasz klucza lub tokenu albo podasz nieprawidłowy, dostaniesz `401 unauthorized`.

Przy wysyłaniu części pliku do `PUT /v1/transcriptions/:id/upload/chunks/:index` użyj nagłówka `X-Upload-Token`. Token otrzymasz, gdy utworzysz upload. Szczegóły znajdziesz [w dokumentacji uploadu](https://transcript1.com/pl/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())
```

## Idempotentność

Dodaj `Idempotency-Key` do `POST /v1/transcriptions`, żeby przy ponowieniu requestu nie tworzyć drugiej transkrypcji.

Klucz musi mieć od 1 do 255 znaków. Przekaż go w nagłówku, nie w parametrach URL-a.

- Gdy ponowisz request z tym samym kluczem i parametrami, dostaniesz pierwotną transkrypcję. Nie zapłacisz ponownie.
- Jeśli zachowasz klucz, ale zmienisz parametry, dostaniesz `409 idempotency_conflict`.
- Jeśli ponawiasz rezerwację uploadu w częściach z tym samym kluczem, dostaniesz nowy token uploadu.

```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())
```

## Obsługa błędów

- Gdy błąd dotyczy requestu, API nie przyjęło wywołania.
- Gdy nie udała się sama transkrypcja, API wcześniej przyjęło zadanie, ale przetwarzanie zakończyło się błędem. Pole `status` ma wtedy wartość `failed`.

### Jak odczytać błąd

Jeśli request do `/v1` zakończy się błędem, w odpowiedzi dostaniesz obiekt `error` z polami `code`, `message` i `retryable`.

- W kodzie aplikacji rozróżniaj błędy po `code`.
- Użytkownikowi wyświetl `message`. Nie opieraj na nim logiki, bo treść komunikatu może się zmienić.
- Sprawdź `retryable`: jeśli to `true`, ten sam request może się udać przy kolejnej próbie.

Zależnie od błędu obok `retryable` mogą pojawić się dodatkowe pola:

| Pole | Kod błędu | Co zawiera |
|---|---|---|
| `action_url` | `insufficient_minutes` | Link do zakupu planu lub doładowania. |
| `minutes_needed`, `minutes_remaining` | `insufficient_minutes` | Ile minut wymaga request i ile masz na koncie. Jeśli nie znamy jeszcze potrzebnej liczby minut, dostaniesz `null`. |
| `supported_platforms` | `unsupported_platform` | Lista obsługiwanych platform: `["youtube", "x", "tiktok", "instagram", "facebook", "bluesky", "twitch", "dailymotion", "pinterest", "linkedin", "snapchat"]`. |
| `missing_chunks` | `upload_incomplete` | Indeksy brakujących części pliku. |
| `max_bytes` | `media_too_large` | Twój limit rozmiaru pliku w bajtach. |
| `params` | `invalid_request` | Obiekt z opisem problemu przy każdym niepoprawnym polu body lub parametrze URL-a. |

### Gdy API nie przyjmie requestu

| Status HTTP | `code` | Ponowić request? | Przyczyna i dalsze kroki |
|---|---|---|---|
| 400, 422 | `invalid_request` | Nie | Brakuje pola w body lub parametru albo ma ono nieprawidłową wartość. Przykłady to `diarize` razem z `source: "captions"` i `reuse_recent` razem z `webhook_url`. Ten kod dostaniesz też za niepoprawny JSON w body. |
| 422 | `invalid_url` | Nie | Podaj link z YouTube, X, TikToka, Instagrama, Facebooka, Bluesky lub innej [obsługiwanej platformy](https://transcript1.com/pl/docs/sources-and-options). Sprawdź też, czy `platform` pasuje do adresu. |
| 422 | `unsupported_platform` | Nie | Wartości podanej w `platform` nie ma na liście obsługiwanych platform. |
| 422 | `unsupported_media` | Nie | Plik ma rozszerzenie, którego nie obsługujemy. |
| 401 | `unauthorized` | Nie | Brakuje klucza lub tokenu albo jest nieprawidłowy, wygasł lub został unieważniony. Ten błąd dostaniesz też za nieprawidłowy lub wygasły token uploadu. |
| 403 | `insufficient_scope` | Nie | Token OAuth nie ma wymaganego uprawnienia `transcriptions:read` lub `transcriptions:write`. |
| 403 | `email_not_verified` | Nie | Najpierw potwierdź e-mail, potem zleć transkrypcję. |
| 402 | `insufficient_minutes` | Nie | Masz 0 minut lub za mało na tę transkrypcję. |
| 402 | `no_active_plan` | Nie | Chcesz dokupić minuty, ale nie masz aktywnego płatnego planu. Ten błąd zwraca endpoint rozliczeń panelu, nie endpointy transkrypcji. |
| 404 | `not_found` | Nie | Nie ma takiej transkrypcji lub należy ona do innego zespołu. |
| 409 | `upload_incomplete` | Nie | Nie wszystkie części pliku dotarły. Wyślij brakujące i ponownie zakończ upload. |
| 409 | `invalid_state` | Nie | To zadanie nie czeka na upload. Nie możesz dosyłać części ani kończyć uploadu. |
| 409 | `idempotency_conflict` | Nie | Wcześniej użyto tego `Idempotency-Key` z innymi parametrami. |
| 413 | `media_too_large` | Nie | Plik przekracza twój limit. [Sprawdź zasady uploadu](https://transcript1.com/pl/docs/uploads). |
| 413 | `invalid_request` | Nie | Body w JSON przekracza 1 MB. Dla zbyt dużego pliku dostaniesz zamiast tego `media_too_large`. |
| 429 | `rate_limited` | Tak | Konto przekroczyło limit requestów na minutę. Odczekaj liczbę sekund z `Retry-After`. |
| 429 | `too_many_active_jobs` | Tak | Masz za dużo trwających transkrypcji lub otwartych uploadów. Odczekaj liczbę sekund z `Retry-After`. |
| 500 | `internal_error` | Tak | Błąd po naszej stronie. Spróbuj za chwilę. |
| 503 | `service_unavailable` | Tak | transcript1 chwilowo nie działa. Spróbuj za chwilę. |

Gdy przeglądarka korzysta z cookie zalogowanej sesji, sprawdzamy też origin requestu. Jeśli request pochodzi z innej strony, zwrócimy `403 forbidden_origin`.

Ta kontrola nie dotyczy wywołań API z nagłówkiem `Authorization`.

Przy każdym `429` dostajesz `Retry-After` z liczbą sekund do następnej próby. [Sprawdź limity swojego planu](https://transcript1.com/pl/docs/billing-and-limits).

### Kiedy spróbować ponownie

- Ponawiaj request bez zmian tylko wtedy, gdy `retryable` to `true`. Dotyczy to `rate_limited`, `too_many_active_jobs`, `internal_error` i `service_unavailable`.
- Jeśli odpowiedź zawiera `Retry-After`, odczekaj podany czas. Bez tego nagłówka wydłużaj przerwę po każdej próbie.
- Dla kolejnych prób `POST /v1/transcriptions` zachowaj ten sam `Idempotency-Key`. Jeśli pierwsze wywołanie się udało, unikniesz ponownej opłaty.
- Przy pozostałych kodach najpierw popraw request.

### Gdy nie uda się przetworzyć nagrania

Jeśli przetwarzanie się nie uda, transkrypcja ma `status` równy `failed`. Jej obiekt `error` zawiera `code` i `message`.

Błąd może wystąpić już po przyjęciu requestu. Sprawdzaj `error`, gdy pobierasz status lub odbierasz [webhook](https://transcript1.com/pl/docs/api-reference#webhooks) `transcription.failed`.

| `error.code` | Co się stało | Co zrobić |
|---|---|---|
| `media_unavailable` | Film nie istnieje lub nie da się go już odtworzyć. | Nie ponawiaj. Sprawdź URL. |
| `media_private` | Film jest prywatny. | Nie ponawiaj. Podaj publiczny post albo film z YouTube: publiczny lub niepubliczny (unlisted). |
| `login_required` | Platforma wymaga logowania. | Nie ponawiaj. To błąd trwały, bez automatycznych ponowień. |
| `no_audio` | Film nie ma ścieżki audio. | Nie ponawiaj. To błąd trwały, bez automatycznych ponowień. |
| `age_restricted` | Film ma ograniczenie wiekowe. | Nie ponawiaj z tym linkiem. Wyślij plik. |
| `live_stream` | Transmisja jeszcze trwa lub nie zdążyła się rozpocząć. | Spróbuj po zakończeniu transmisji. |
| `no_captions` | W `source` podano `captions`, ale brakuje napisów, z których da się skorzystać. | Zmień na `source: "auto"` lub `"stt"`. |
| `media_too_long` | Nagranie przekracza limit 5 godzin dla transkrypcji audio. | Nie ponawiaj w całości. Podziel nagranie. |
| `unsupported_media` | Nie możemy odczytać pliku lub plik nie zawiera audio. | Nie ponawiaj z tym plikiem. Wyślij plik w obsługiwanym formacie. |
| `no_speech` | Nie wykryto mowy. | Nie ponawiaj. |
| `unsupported_language` | Nie obsługujemy języka nagrania. | Nie ponawiaj bez zmian. W `language` podaj obsługiwany kod lub `"auto"`. |
| `insufficient_minutes` | Zabrakło minut, zanim zdążyliśmy rozliczyć transkrypcję. | Dokup minuty i spróbuj ponownie. |
| `fetch_failed` | Mimo kilku prób nie udało się pobrać nagrania. | Spróbuj ponownie później. |
| `transcription_failed` | Mimo kilku prób nie udało się przepisać audio. | Spróbuj ponownie później. |

Jeśli transkrypcja się nie uda albo ją anulujesz, nic nie płacisz. Oddajemy też wszystkie minuty, które już pobraliśmy za to nagranie.

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

- `id` (string, required)
- `object` (string, required, one of `transcription`)
- `status` (string, required, one of `awaiting_upload`, `queued`, `fetching`, `transcribing`, `completed`, `failed`, `canceled`)
- `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)
- `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)
  - `source` (string, required, one of `auto`, `captions`, `stt`)
  - `language` (string, required)
  - `diarize` (boolean, required)
- `transcript_source` (string, required, nullable, one of `youtube_captions`, `x_captions`, `speech_to_text`)
- `caption_kind` (string, required, nullable, one of `manual`, `auto_generated`)
- `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`.
- `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)
    - `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
{
  "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 YouTube/X-only. 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 YouTube or X captions when available, otherwise speech-to-text. "captions" is YouTube/X-only. "stt" always uses speech-to-text.
  - `language` (string, optional)
  - `diarize` (boolean, optional)
  - `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)
  - `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)
  - `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
{
  "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
{
  "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
{
  "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.
- `format` (string, optional, default json, one of `json`, `text`, `srt`, `vtt`)
- `include` (string, optional, one of `words`)
- `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
{
  "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 · Failed:

```json
{
  "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
{
  "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`)
- `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": [
    {
      "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.

#### 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
- 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 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
{
  "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 and rate limits.

### 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)

#### 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
  }
}
```

### Get your account

`GET /v1/account`

Returns the authenticated user, current plan and remaining transcription minutes.

#### 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
  }
}
```

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
  }
}
```

## Webhooki

Przy tworzeniu transkrypcji podaj `webhook_url`. Gdy tekst będzie gotowy albo przetwarzanie się nie uda, transcript1 wyśle na ten adres request `POST` z podpisem.

Z webhooków skorzystasz tylko przez REST API. W [serwerze MCP](https://transcript1.com/pl/docs/mcp) nie podasz adresu webhooka.

### Gdzie odbierać webhooki

Dodaj `webhook_url` do wywołania `POST /v1/transcriptions`:

- Przy linku do filmu lub rezerwacji uploadu w częściach umieść go w body w JSON.
- Jeśli wysyłasz cały plik jednym requestem, użyj pola formularza.

Sprawdź adres przed wysłaniem:

- Musi korzystać z HTTPS.
- Nie może zawierać nazwy użytkownika ani hasła.
- Musi wskazywać publiczny adres.

Adres prywatny, loopback, link-local lub inny niepubliczny spowoduje `422 invalid_request`. Sprawdzamy go przy tworzeniu transkrypcji i ponownie przed każdym wysłaniem webhooka.

Nie łącz `webhook_url` z `reuse_recent=true`.

### Kiedy dostaniesz powiadomienie

| `type` | Co wywołuje webhook |
|---|---|
| `transcription.completed` | Zakończenie transkrypcji ze statusem `completed`. |
| `transcription.failed` | Błąd przetwarzania i status `failed`. W `data.error` znajdziesz `code` i `message`. |

Jeśli anulujesz transkrypcję, nie wyślemy webhooka.

### Co zawiera webhook

W body requestu dostajesz JSON:

- W `created_at` znajdziesz czas utworzenia zdarzenia.
- W `data` dostaniesz obiekt transkrypcji, ale bez `transcript`.

Sam tekst pobierz przez `GET /v1/transcriptions/:id`. Użyj `id` z obiektu `data`.

Z każdym requestem wysyłamy nagłówki:

- `Content-Type: application/json`
- `User-Agent: transcript1-webhooks/1.0`
- `X-Transcript1-Signature` z podpisem opisanym poniżej.

### Jak sprawdzić podpis

W każdym webhooku znajdziesz `X-Transcript1-Signature` w formacie `t=<unix seconds>,v1=<hex digest>`.

- `t` podaje czas podpisania requestu jako Unix time w sekundach.
- `v1` to HMAC-SHA256 zapisany szesnastkowo. Oblicz go dla `"<t>.<raw body>"`: połącz wartość `t`, kropkę i body dokładnie w takiej postaci, w jakiej dotarło. Kluczem jest cały sekret do podpisywania webhooków, łącznie z prefiksem `whsec_`.

Sprawdź podpis na oryginalnych bajtach body, zanim sparsujesz JSON. Parsowanie i ponowny zapis mogą zmienić białe znaki lub kolejność kluczy. Podpis przestanie wtedy pasować.

- Do porównania podpisów użyj funkcji działającej w stałym czasie.
- Odrzuć request, jeśli czas w `t` zbyt mocno odbiega od bieżącego. W przykładach dopuszczamy różnicę 5 minut.
- W Expressie odczytaj body jako surowy tekst, bez wcześniejszego parsowania.
- We Flasku użyj `request.get_data(as_text=True)` jako `raw_body`.

### Sekret do podpisywania webhooków

Zespół ma wspólny sekret z prefiksem `whsec_`. Używamy go do podpisywania webhooków wszystkich transkrypcji tego zespołu.

Sekret sprawdzisz lub wymienisz na nowy na stronie [„Webhooks”](https://transcript1.com/dashboard/webhooks).

Zmiana działa od razu. Każdy webhook wysłany po wymianie sekretu podpisujemy już nowym.

### Potwierdzanie odbioru i kolejne próby

- Odpowiedz dowolnym statusem `2xx`, żeby potwierdzić odbiór.
- Inny status, błąd połączenia lub brak odpowiedzi przez 10 sekund oznacza, że próba się nie udała.
- Jeśli nie dostaniemy potwierdzenia, ponowimy wysyłkę. Łącznie wykonujemy do 6 prób z przerwami 2, 4, 8, 16 i 32 minuty. Ostatnia wypada około godziny po pierwszej.
- Podaj docelowy URL, bez przekierowań. transcript1 ich nie obsługuje, więc `3xx` oznacza nieudaną próbę.
- Ten sam webhook może dotrzeć więcej niż raz, np. gdy twój serwer odpowie dopiero po limicie czasu. Rozpoznawaj duplikaty po `data.id` i `type`.

```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
  }
}
```
