# API referansı

> Tüm /v1 endpoint'lerini parametreleri ve örnekleriyle inceleyin. OpenAPI tanımını JSON veya YAML olarak indirin.

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

## Giriş

Tüm endpoint'ler için `https://api.transcript1.com/v1` adresini temel alın.

- İstek gövdesini JSON olarak gönderin. Yanıtlar da JSON'dur; alan adlarında `snake_case` kullanılır.
- Tarih ve saatler UTC üzerinden ISO 8601 biçiminde verilir: `2026-10-13T09:30:00.000Z` gibi.
- Kayıt süresini ve transkriptteki zamanları saniye olarak alırsınız.
- Hata yanıtlarının yapısı ortaktır. Alanları [Hatalar](https://transcript1.com/tr/docs/api-reference#errors) bölümünden inceleyebilirsiniz.
- Kimlik doğrulaması yapılmış her çağrının yanıtında `X-RateLimit-Limit` ve `X-RateLimit-Remaining` başlıkları bulunur. Ayrıntılar [Ücretlendirme ve limitler](https://transcript1.com/tr/docs/billing-and-limits) sayfasında.
- `source=auto` ile YouTube, X, TikTok, Bluesky, Dailymotion ve LinkedIn'de önce videonun kendi dilindeki kullanılabilir altyazıyı ararız. Bulamazsak sesi STT ile yazıya çeviririz. `source=captions` (yalnızca altyazı) seçerseniz ses indirmeyiz, STT'ye de geçmeyiz; kullanılabilir altyazı yoksa `no_captions` alırsınız. `source=stt` veya konuşmacı ayırma ise altyazı adımını atlar. Platform kısıtlamalarını, segment ve kelime zamanları arasındaki farkları [Kaynaklar ve seçenekler](https://transcript1.com/tr/docs/sources-and-options) sayfasından kontrol edin.

Araçlarınızda kullanmak için OpenAPI tanımını indirin:

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

## Kimlik doğrulama

API anahtarınızı veya OAuth token'ınızı `Authorization` başlığına ekleyin. Kullanılacak biçim: `Authorization: Bearer <token>`.

- **API anahtarı:** `t1_` ile başlar ve tüm endpoint'lerde kullanılabilir. Paneldeki [“API keys” sayfasından](https://transcript1.com/dashboard/account/tokens) oluşturun.
- **OAuth erişim token'ı:** `t1o_` ile başlar. Claude veya ChatGPT gibi bir MCP istemcisini bağladığınızda istemciye verilir. `transcriptions:read` ve `transcriptions:write` yetkilerini kullanır.

| Yetki kapsamı | Kullanılabilen çağrılar |
|---|---|
| `transcriptions:read` | `GET /v1/transcriptions`, `GET /v1/transcriptions/:id`, `GET /v1/transcriptions/:id/upload` ve `GET /v1/account` |
| `transcriptions:write` | `POST /v1/transcriptions`, `DELETE /v1/transcriptions/:id` ve `POST /v1/transcriptions/:id/upload/complete` |

- Token gereken yetki kapsamına sahip değilse `403 insufficient_scope` alırsınız.
- Kimlik bilgisi göndermezseniz veya gönderdiğiniz bilgi geçersizse `401 unauthorized` döner.

Dosya parçalarını gönderen `PUT /v1/transcriptions/:id/upload/chunks/:index` çağrısında ise `X-Upload-Token` kullanın. Bu token yüklemeyi başlattığınızda verilir. Adımlar için [Dosya yükleme](https://transcript1.com/tr/docs/uploads) sayfasına bakın.

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

Bağlantı kesildiğinde veya yeniden denemeniz gerektiğinde aynı transkripsiyonu iki kez başlatmayın. `POST /v1/transcriptions` çağrısına `Idempotency-Key` başlığı ekleyin.

Anahtar 1–255 karakter uzunluğunda olmalıdır. Sorgu parametresine yazmayın; HTTP başlığında gönderin.

- Aynı anahtar ve parametrelerle tekrar çağırırsanız ilk transkripsiyonu alırsınız. Yeniden dakika harcamazsınız.
- Parametreleri değiştirip anahtarı aynı bırakırsanız `409 idempotency_conflict` alırsınız.
- Parçalı yüklemeyi başlatan isteği aynı anahtarla tekrarladığınızda yeni bir yükleme token'ı alırsınız.

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

## Hatalar

- İstek hatası alırsanız API çağrınızı kabul etmemiştir.
- Transkripsiyon hatasında istek kabul edilmiştir, ancak işlem daha sonra başarısız olmuştur. Transkripsiyonun `status` alanı `failed` olur.

### Hata yanıtının yapısı

`/v1` çağrılarında istek hatalarını `error` nesnesi içinde alırsınız. Bu nesnede `code`, `message` ve `retryable` bulunur.

- Uygulamanızın atacağı adımı `code` üzerinden belirleyin.
- `message` kullanıcıya gösterebileceğiniz açıklamadır. Metni değişebileceği için bu alana göre koşul yazmayın.
- `retryable` değeri `true` olduğunda aynı çağrı daha sonra çalışabilir.

Bazı hatalarda `retryable` yanında sorunu açıklayan ek alanlar da bulunur:

| Alan | İlgili hata | Açıklama |
|---|---|---|
| `action_url` | `insufficient_minutes` | Plan seçebileceğiniz veya ek dakika satın alabileceğiniz sayfa. |
| `minutes_needed`, `minutes_remaining` | `insufficient_minutes` | İşlem için gereken dakika ve kullanılabilir bakiyeniz. Gereken süre henüz bilinmiyorsa ilk alan `null` olur. |
| `supported_platforms` | `unsupported_platform` | Kabul edilen platform değerleri: `["youtube", "x", "tiktok", "instagram", "facebook", "bluesky", "twitch", "dailymotion", "pinterest", "linkedin", "snapchat"]`. |
| `missing_chunks` | `upload_incomplete` | Sunucuya ulaşmayan parçaların numaraları. |
| `max_bytes` | `media_too_large` | Hesabınızın dosya boyutu limiti, bayt olarak. |
| `params` | `invalid_request` | Gövde veya sorgu doğrulamasında her hatalı alanı ve o alandaki sorunu gösteren nesne. |

### API'nin isteği kabul etmediği durumlar

| HTTP kodu | `code` | Aynı istek tekrar denenebilir mi? | Neden alırsınız? |
|---|---|---|---|
| 400, 422 | `invalid_request` | Hayır | Bir parametre veya gövde alanı eksik ya da geçersiz. Örneğin `diarize` ile `source: "captions"` veya `reuse_recent` ile `webhook_url` birlikte gönderilmiş olabilir. Gövde geçerli JSON değilse de bu kod döner. |
| 422 | `invalid_url` | Hayır | Bağlantı desteklenmiyor veya `platform` ile eşleşmiyor. YouTube, X, TikTok, Instagram, Facebook, Bluesky ya da [Kaynaklar ve seçenekler](https://transcript1.com/tr/docs/sources-and-options) sayfasındaki diğer platformlardan birinin bağlantısını kullanın. |
| 422 | `unsupported_platform` | Hayır | Gönderdiğiniz `platform` değeri desteklenenler arasında değil. |
| 422 | `unsupported_media` | Hayır | Dosyanın uzantısını desteklemiyoruz. |
| 401 | `unauthorized` | Hayır | API kimlik bilginiz eksik, geçersiz, süresi dolmuş veya iptal edilmiş. Geçersiz ya da süresi dolmuş yükleme token'ında da bu kod döner. |
| 403 | `insufficient_scope` | Hayır | OAuth token'ında gereken `transcriptions:read` veya `transcriptions:write` yetkisi yok. |
| 403 | `email_not_verified` | Hayır | E-postanız henüz doğrulanmamış. Transkripsiyon başlatmadan önce adresinizi doğrulayın. |
| 402 | `insufficient_minutes` | Hayır | Kullanılabilir bakiyeniz 0 veya işlem için gereken dakikadan az. |
| 402 | `no_active_plan` | Hayır | Ücretli planınız aktif değilken ek dakika satın almaya çalıştınız. Bu kod yalnızca panelin ücretlendirme endpoint'inden döner; transkripsiyon endpoint'lerinden dönmez. |
| 404 | `not_found` | Hayır | Bu transkripsiyon bulunamadı veya başka bir ekibe ait. |
| 409 | `upload_incomplete` | Hayır | Dosyanın bazı parçaları yüklenmemiş. Eksikleri gönderip tamamlama çağrısını tekrarlayın. |
| 409 | `invalid_state` | Hayır | Transkripsiyon yükleme bekleme aşamasında değil. Artık parça gönderemez veya yüklemeyi tamamlayamazsınız. |
| 409 | `idempotency_conflict` | Hayır | Aynı `Idempotency-Key` daha önce farklı parametrelerle kullanılmış. |
| 413 | `media_too_large` | Hayır | Dosya, hesabınızın boyut limitini aşıyor. [Dosya yükleme](https://transcript1.com/tr/docs/uploads) sayfasını kontrol edin. |
| 413 | `invalid_request` | Hayır | JSON gövdesi 1 MB'ı aşıyor. Dosya boyutu hatalarında bunun yerine `media_too_large` kullanılır. |
| 429 | `rate_limited` | Evet | Dakikalık istek limitini aştınız. `Retry-After` değerinde belirtilen saniye kadar bekleyin. |
| 429 | `too_many_active_jobs` | Evet | Aynı anda çalışan transkripsiyon veya açık yükleme limitini aştınız. `Retry-After` değerinde belirtilen saniye kadar bekleyin. |
| 500 | `internal_error` | Evet | Sunucu tarafında bir sorun oluştu. Biraz bekleyip tekrar deneyin. |
| 503 | `service_unavailable` | Evet | transcript1'e şu anda erişilemiyor. Biraz bekleyip tekrar deneyin. |

Tarayıcıdan oturum çereziyle yapılan çağrılarda isteğin geldiği siteyi de kontrol ederiz. Başka bir siteden geliyorsa `403 forbidden_origin` döner.

Bu kontrol, `Authorization` başlığıyla yaptığınız API çağrılarını etkilemez.

Tüm `429` yanıtlarında `Retry-After` bulunur. Değer saniye cinsindedir. Hesabınıza uygulanan limitleri [Ücretlendirme ve limitler](https://transcript1.com/tr/docs/billing-and-limits) sayfasından kontrol edebilirsiniz.

### Ne zaman tekrar denemelisiniz?

- Aynı isteği yalnızca `retryable` değeri `true` olduğunda tekrar gönderin: `rate_limited`, `too_many_active_jobs`, `internal_error` ve `service_unavailable`.
- `Retry-After` varsa belirtilen süreyi bekleyin. Yoksa her denemede bekleme süresini artırın.
- `POST /v1/transcriptions` çağrısını tekrarlarken aynı `Idempotency-Key` başlığını koruyun. İlk çağrı aslında başarılı olduysa böylece ikinci kez dakika harcamazsınız.
- Diğer kodlarda önce hatanın nedenini giderin, sonra yeniden gönderin.

### İşlem başladıktan sonra oluşan hatalar

Transkripsiyonun `status` değeri `failed` olduğunda, `code` ve `message` içeren bir `error` nesnesi de alırsınız.

İstek kabul edildikten sonra da hata oluşabilir. Durumu sorgularken veya `transcription.failed` [webhook bildirimi](https://transcript1.com/tr/docs/api-reference#webhooks) aldığınızda `error` alanını kontrol edin.

| `error.code` | Ne oldu? | Ne yapmalısınız? |
|---|---|---|
| `media_unavailable` | Video bulunamadı veya artık erişilemiyor. | Aynı bağlantıyla tekrar denemeyin; URL'yi kontrol edin. |
| `media_private` | Video gizli. | Aynı videoyu tekrar göndermeyin. Herkese açık bir gönderi kullanın; YouTube'da liste dışı videolar da çalışır. |
| `login_required` | Platform videoyu izlemek için giriş yapılmasını istiyor. | Kalıcı bir hatadır; tekrar denemeyiz. |
| `no_audio` | Videoda ses kanalı yok. | Kalıcı bir hatadır; tekrar denemeyiz. |
| `age_restricted` | Videoda yaş kısıtlaması var. | Aynı bağlantıyı tekrar göndermek yerine dosyayı yükleyin. |
| `live_stream` | Yayın devam ediyor veya henüz başlamamış. | Yayın sona erdikten sonra deneyin. |
| `no_captions` | `source` için `captions` seçilmiş, ancak kullanılabilir altyazı bulunamamış. | `source: "auto"` veya `"stt"` ile gönderin. |
| `media_too_long` | Kayıt, STT için geçerli 5 saat sınırını aşıyor. | Aynı kaydı tekrar göndermeyin; daha kısa parçalara bölün. |
| `unsupported_media` | Dosyayı okuyamadık veya dosyada ses kanalı yok. | Desteklenen bir dosya hazırlayıp gönderin. |
| `no_speech` | Konuşma algılanmadı. | Tekrar denemeyin. |
| `unsupported_language` | Kayıttaki dili desteklemiyoruz. | Aynı ayarla tekrar denemeyin. `language` için desteklenen bir kod veya `"auto"` seçin. |
| `insufficient_minutes` | Ücret hesaplanmadan önce dakika bakiyeniz tükendi. | Ek dakika yükledikten sonra deneyin. |
| `fetch_failed` | Birkaç kez denememize rağmen kaydı indiremedik. | Daha sonra tekrar deneyebilirsiniz. |
| `transcription_failed` | Birkaç kez denememize rağmen STT tamamlanamadı. | Daha sonra tekrar deneyebilirsiniz. |

Transkripsiyon başarısız olursa veya iptal edilirse ücret ödemezsiniz. Daha önce dakika düşülmüşse hesabınıza geri ekleriz.

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

## Webhook bildirimleri

İşlem sona erdiğinde bildirim almak için transkripsiyonu oluştururken `webhook_url` gönderin. Transkripsiyon tamamlanırsa veya başarısız olursa bu adrese imzalı bir `POST` yollarız.

Bu özellik yalnızca REST API'de bulunur. [MCP sunucusu](https://transcript1.com/tr/docs/mcp) webhook parametresi kabul etmez.

### Bildirim adresini belirtin

`POST /v1/transcriptions` çağrısında `webhook_url` alanını ekleyin:

- Video bağlantısı gönderirken veya parçalı yükleme başlatırken JSON gövdesini kullanın.
- Dosyayı tek istekte yüklüyorsanız aynı alanı forma ekleyin.

Bildirim adresiniz için şu kurallar geçerlidir:

- HTTPS zorunludur.
- URL içinde kullanıcı adı veya parola bulunamaz.
- Adres, internetten erişilebilen bir IP adresine çözümlenmelidir.

Özel ağ, loopback, link-local veya diğer herkese açık olmayan adresler için `422 invalid_request` alırsınız. Adresi hem transkripsiyon oluşturulurken hem de her bildirim gönderiminde kontrol ederiz.

`webhook_url` gönderiyorsanız aynı istekte `reuse_recent=true` kullanmayın; bu iki seçenek birlikte çalışmaz.

### Hangi olaylarda bildirim gelir?

| `type` | Gönderildiği durum |
|---|---|
| `transcription.completed` | Transkripsiyon tamamlandı; durumu `completed`. |
| `transcription.failed` | Transkripsiyon başarısız oldu; durumu `failed`. Hatanın `code` ve `message` alanları `data.error` içinde bulunur. |

Transkripsiyonu iptal ederseniz webhook göndermeyiz.

### Bildirimin içeriği

Gelen isteğin JSON gövdesinde şu bilgiler bulunur:

- `created_at`: Olayın oluştuğu an.
- `data`: Transkripsiyon nesnesi; transkript metni olan `transcript` bu nesneye eklenmez.

Metni almak için `data` içindeki `id` değerini kullanarak `GET /v1/transcriptions/:id` çağırın.

Her bildirimde aşağıdaki HTTP başlıklarını göndeririz:

- `Content-Type: application/json`
- `User-Agent: transcript1-webhooks/1.0`
- İmzayı taşıyan `X-Transcript1-Signature`; doğrulama adımları aşağıda.

### Gelen isteğin imzasını doğrulayın

`X-Transcript1-Signature` başlığı her gönderimde bulunur. Değeri `t=<unix seconds>,v1=<hex digest>` biçimindedir.

- `t`, imzanın oluşturulduğu anı saniye cinsinden Unix zamanı olarak verir.
- `v1`, `"<t>.<raw body>"` metninden hesaplanan HMAC-SHA256 imzasının hex biçimidir. `t` değerini, bir noktayı ve hiç değiştirmediğiniz istek gövdesini birleştirin. HMAC anahtarı olarak gizli imza anahtarınızın tamamını, `whsec_` önekiyle birlikte kullanın.

İmzayı JSON'u ayrıştırmadan, gövdenin gelen baytları üzerinden doğrulayın. JSON'u ayrıştırıp yeniden oluşturmak boşlukları veya alan sırasını değiştirebilir; bu durumda imza eşleşmez.

- İmzaları sabit zamanda çalışan bir karşılaştırmayla kontrol edin.
- `t` ile şu an arasındaki fark çok büyükse isteği reddedin. Örnekler 5 dakikalık farka izin verir.
- Express kullanıyorsanız gövdeyi JSON'a çevirmeden ham metin olarak alın.
- Flask kullanıyorsanız `raw_body` için `request.get_data(as_text=True)` değerini gönderin.

### Webhook imza anahtarınız

Gizli imza anahtarınız `whsec_` ile başlar. Her ekip için tek anahtar vardır; ekibin tüm transkripsiyonlarına ait webhook bildirimlerini bununla imzalarız.

Anahtarı görmek veya yenilemek için paneldeki [“Webhooks” sayfasını](https://transcript1.com/dashboard/webhooks) açın.

Yenilediğiniz anda eski anahtarın yerini yenisi alır. O andan sonra gönderdiğimiz bildirimleri yeni anahtarla imzalarız.

### Gönderim ve yeniden deneme kuralları

- Sunucunuz `2xx` döndürürse bildirimi teslim edilmiş sayarız.
- Başka bir durum kodu alırsak, bağlantı kurulamazsa veya 10 saniye içinde yanıt gelmezse gönderim başarısız sayılır.
- Başarısız gönderimleri giderek daha uzun aralıklarla tekrarlarız. İlk gönderim dahil en fazla 6 deneme yaparız. Aralarındaki bekleme süreleri 2, 4, 8, 16 ve 32 dakikadır. Son deneme, ilkinden yaklaşık bir saat sonra yapılır.
- Doğrudan bildirimi işleyecek URL'yi belirtin. Yönlendirmeleri takip etmeyiz; `3xx` yanıtını başarısız sayarız.
- Aynı bildirim birden fazla kez gelebilir. Örneğin sunucunuz zaman aşımından sonra yanıt verdiğinde yeniden gönderim yapabiliriz. `data.id` ve `type` çiftini kontrol ederek aynı olayı tekrar işlemeyin.

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