# Webhook kullanımı

> Transkripsiyon tamamlandığında veya başarısız olduğunda bildirim alın. Gelen isteğin imzasını doğrulayın.

Canonical: https://transcript1.com/tr/docs/webhooks
Language: tr
Last updated: 2026-10-14

Transkripsiyonun bitmesini sürekli sorgulamak yerine bildirim alabilirsiniz. Oluşturma isteğine `webhook_url` ekleyin. İşlem tamamlanırsa veya başarısız olursa bu adrese imzalı bir `POST` göndeririz.

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

## Bildirim adresini belirtin

Video bağlantısı gönderirken veya parçalı yüklemeyi başlatırken `webhook_url` alanını `POST /v1/transcriptions` isteğinin JSON gövdesine ekleyin. Dosyayı tek istekte yüklüyorsanız aynı adı taşıyan form alanını kullanın:

```json
{
  "platform": "youtube",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "webhook_url": "https://example.com/hooks/transcript1"
}
```

Adres HTTPS kullanmalı ve internetten erişilebilen bir IP adresine çözümlenmelidir. URL'ye kullanıcı adı veya parola eklemeyin. Ö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

Bildirimde aşağıdaki gibi bir JSON gövdesi alırsınız:

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

Olayın ne zaman oluştuğunu `created_at` gösterir. `data` içinde transkripsiyon nesnesi bulunur, ancak transkript metni (`transcript`) gönderilmez. Yukarıdaki nesne kısaltılmıştır. Metni almak için `data` içindeki `id` değerini kullanarak `GET /v1/transcriptions/:id` çağrısı yapın.

Her istekte `Content-Type: application/json` ve `User-Agent` değeri olarak `transcript1-webhooks/1.0` göndeririz. Ayrıca aşağıda açıklanan imza başlığı bulunur.

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

İmza her gönderimde `X-Transcript1-Signature` başlığındadır:

```text
X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>
```

- `t`: İsteğin imzalandığı anı, saniye cinsinden Unix zamanı olarak verir.
- `v1`: `"<t>.<raw body>"` metninden hesaplanan HMAC-SHA256 imzasının hex biçimidir. Önce `t` değerini, sonra bir noktayı, ardından 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şturursanız boşluklar veya alan sırası değişebilir ve imza eşleşmez. Karşılaştırma sabit zamanda çalışmalıdır. `t` ile şu an arasındaki fark çok büyükse isteği reddedin; aşağıdaki örnekler 5 dakikalık farka izin verir.

### Node.js

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

Express kullanıyorsanız gövdeyi JSON'a çevirmeden ham metin olarak alın:

```javascript
import process from 'node:process';
import express from 'express';
import { verifySignature } from './verify-signature.js';

const app = express();

app.post('/hooks/transcript1', express.text({ type: 'application/json' }), (request, response) => {
  const header = request.get('X-Transcript1-Signature') ?? '';

  if (!verifySignature(process.env.TRANSCRIPT1_WEBHOOK_SECRET, header, request.body)) {
    response.sendStatus(400);
    return;
  }

  const { type, data } = JSON.parse(request.body);
  console.log(type, data.id, data.status);
  response.sendStatus(200);
});
```

### Python

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

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 (`/dashboard/webhooks`). 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.
- Yönlendirmeleri takip etmeyiz. `3xx` yanıtını da başarısız sayarız; bu yüzden doğrudan bildirimi işleyecek URL'yi belirtin.
- 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.
