# Powiadomienia przez webhooki

> Odbierz podpisany request, gdy transkrypcja będzie gotowa albo się nie uda. Sprawdź, jak zweryfikować jego podpis.

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

Dodaj `webhook_url` przy zlecaniu transkrypcji. Gdy będzie gotowa albo zakończy się błędem, wyślemy pod ten adres podpisany request `POST`.

Webhooki skonfigurujesz tylko przez REST API. W [serwerze MCP](https://transcript1.com/pl/docs/mcp) nie ma parametru, który je ustawia.

## Podaj adres swojego endpointu

Jeśli wysyłasz obsługiwany link do nagrania lub rezerwujesz upload w częściach, dodaj `webhook_url` do JSON-a w `POST /v1/transcriptions`. Przy uploadzie jednym requestem przekaż go jako pole formularza:

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

Użyj HTTPS i adresu, który wskazuje na publiczny adres IP. URL nie może zawierać nazwy użytkownika ani hasła. Jeśli wskazuje na adres prywatny, loopback, link-local lub inny niepubliczny, API zwróci `422 invalid_request`. Sprawdzamy adres przy tworzeniu transkrypcji i przed każdym wysłaniem webhooka.

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

## Kiedy wysyłamy webhooki

| `type` | Kiedy go wyślemy |
|---|---|
| `transcription.completed` | Gdy transkrypcja ma już status `completed`. |
| `transcription.failed` | Gdy transkrypcja kończy się ze statusem `failed`. W `data.error` znajdziesz `code` i `message`. |

Anulowanie transkrypcji nie uruchamia webhooka.

## Zawartość webhooka

W body requestu dostaniesz JSON:

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

Pole `created_at` podaje czas utworzenia zdarzenia. W `data` znajdziesz obiekt transkrypcji, ale bez `transcript`. Powyżej pokazujemy go w skrócie. Sam tekst pobierz przez `GET /v1/transcriptions/:id`, podstawiając `id` z `data`.

Request ma nagłówek `Content-Type: application/json`, `User-Agent` ustawiony na `transcript1-webhooks/1.0` oraz podpis, który sprawdzisz w kolejnym kroku.

## Sprawdź podpis przed odczytaniem JSON-a

Podpis każdego requestu znajdziesz w nagłówku `X-Transcript1-Signature`:

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

- `t` podaje czas podpisania requestu jako Unix timestamp w sekundach.
- `v1` to HMAC-SHA256 zapisany szesnastkowo. Liczymy go dla ciągu `"<t>.<raw body>"`, czyli wartości `t`, kropki i body dokładnie w takiej postaci, w jakiej dotarło. Do obliczenia używamy całego sekretu, razem z prefiksem `whsec_`.

Podpis sprawdzaj na oryginalnych bajtach body, zanim sparsujesz JSON. Parsowanie i ponowny zapis JSON-a mogą zmienić odstępy lub kolejność kluczy. Wtedy podpis przestanie pasować. Użyj porównania w stałym czasie. Odrzuć też request, jeśli `t` za bardzo odbiega od aktualnego czasu. W przykładach dopuszczamy różnicę do 5 minut.

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

W Expressie odbierz body jako surowy tekst. Dzięki temu sprawdzisz podpis przed parsowaniem:

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

Jeśli używasz Flaska, do `raw_body` przekaż `request.get_data(as_text=True)`.

## Sekret webhooka i jego rotacja

Zespół ma jeden sekret z prefiksem `whsec_`. Podpisujemy nim webhooki wszystkich transkrypcji tego zespołu. W panelu [„Webhooks”](https://transcript1.com/dashboard/webhooks), pod adresem `/dashboard/webhooks`, możesz go wyświetlić lub wymienić na nowy. Rotacja działa od razu: każdy webhook wysłany po zmianie podpisujemy już nowym sekretem.

## Odpowiedzi, timeout i ponowne próby

- Odpowiedz dowolnym kodem `2xx`, a uznamy webhook za dostarczony.
- Inny kod HTTP, błąd połączenia lub brak odpowiedzi przez 10 sekund oznaczają nieudaną próbę.
- Po nieudanej próbie ponawiamy wysłanie, maksymalnie do 6 prób łącznie. Stosujemy wykładniczy backoff: czekamy kolejno 2, 4, 8, 16 i 32 minuty. Ostatnia próba wypada około godziny po pierwszej.
- Podaj docelowy URL. Nie przechodzimy pod adres z przekierowania, a odpowiedź `3xx` uznajemy za nieudaną próbę.
- To samo zdarzenie może dotrzeć kilka razy, np. gdy twój serwer odpowie już po upływie timeoutu. Rozpoznawaj duplikaty po parze `data.id` i `type`, żeby nie obsługiwać ich ponownie.
