Webhooki
Otrzymuj powiadomienie, gdy rozmowa jest gotowa, zamiast odpytywać API.
Zarejestruj endpoint, a wyślemy na niego żądanie POST, gdy przetwarzanie rozmowy się zakończy, więc nie musisz odpytywać API.
Wszystko, co opisuje ta strona, możesz też zrobić bez pisania kodu, w sekcji Konfiguracja → Webhooki w projekcie. Ten ekran pozwala rejestrować endpointy, wstrzymywać je i przeglądać opisany niżej dziennik dostaw.
Rejestrowanie endpointu
curl -X POST "$BASE/v1/projects/$PROJECT_ID/webhooks" \
-H "X-API-Key: $AFTERTALK_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-app.example.com/hooks/aftertalk","events":["call.processed","call.failed"]}'{
"id": "3f7c...",
"url": "https://your-app.example.com/hooks/aftertalk",
"events": ["call.processed", "call.failed"],
"active": true,
"secret": "whsec_9f2c1e...",
"createdAt": "2026-07-25T10:00:00Z"
}secret jest zwracany jeden raz, przy tworzeniu. Zapisz go — jest potrzebny do weryfikacji podpisów, a lista
webhooków już go nie pokaże.
Adres URL musi być publicznym adresem HTTPS. Zwykłe http, adresy loopback, zakresy prywatne i adresy
metadanych chmury są odrzucane z kodem 400 — to samo sprawdzenie jest wykonywane ponownie przed
każdą dostawą, więc zmiana rekordu DNS tak, by wskazywał adres wewnętrzny, po rejestracji również nie zadziała.
Zdarzenia
| Zdarzenie | Kiedy |
|---|---|
call.processed | Przetwarzanie rozmowy zakończyło się statusem COMPLETED lub BILLING_FAILED, a wyniki zostały zapisane |
call.failed | Przetwarzanie zakończyło się statusem FAILED; nie powstała transkrypcja |
Treść żądania
Treść żądania jest celowo niewielka — identyfikatory, kategoria i wydźwięk — bez transkrypcji:
{
"id": "8f14e45f-ea0f-4e21-9c1a-2b3c4d5e6f70",
"type": "call.processed",
"createdAt": "2026-07-25T10:04:12Z",
"data": {
"callId": "b7e2...",
"projectId": "1c3a...",
"jobId": "8f14...",
"status": "COMPLETED",
"category": "billing",
"sentiment": "neutral"
}
}callId to ta sama wartość co transcriptId w zadaniu przetwarzania. Przy zdarzeniu call.failed pole
errorMessage podaje przyczynę błędu.
Pobierz rozmowę przez GET /calls/{callId}, aby uzyskać transkrypcję i pełną analizę. Dzięki temu,
że transkrypcja nie trafia do żądania, nie wysyłamy treści rozmów na Twój adres URL, a Ty zawsze odczytujesz
aktualne dane zamiast migawki z chwili wysłania.
status może mieć wartość COMPLETED lub BILLING_FAILED. Obie oznaczają, że wyniki można odczytać —
zobacz pobieranie wyników.
Każda dostawa zawiera te nagłówki:
| Nagłówek | Zawartość |
|---|---|
X-Aftertalk-Signature | t=<unix seconds>,v1=<hmac> |
X-Aftertalk-Event-Id | Identyfikator zdarzenia — ta sama wartość co id w treści |
X-Aftertalk-Event-Type | np. call.processed |
Weryfikacja podpisu
Zawsze weryfikuj podpis, zanim zareagujesz na dostawę — w przeciwnym razie każdy, kto pozna Twój adres URL, może na niego wysyłać żądania.
Podpis to HMAC-SHA256 z "<timestamp>.<raw body>", z Twoim sekretem jako kluczem. Znacznik czasu
jest częścią podpisywanego ciągu, więc nie da się go podmienić na świeży; porównaj go z własnym
zegarem i odrzucaj zbyt stare żądania.
import crypto from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verify(rawBody, header, secret) {
const parts = new Map(header.split(',').map((p) => p.split('=')));
const timestamp = Number(parts.get('t'));
const signature = parts.get('v1');
if (!timestamp || !signature) return false;
// Reject replays of an old, otherwise-valid request.
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
// Constant-time compare, so a wrong guess can't be narrowed down by timing.
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}import hashlib, hmac, time
TOLERANCE_SECONDS = 300
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
try:
timestamp = int(parts["t"])
signature = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)Weryfikuj na podstawie surowej treści żądania, przed jakimkolwiek parsowaniem JSON i ponowną serializacją. Ponowne kodowanie zmienia bajty, więc podpis nie będzie się zgadzał.
Ponowienia i idempotentność
Dostawę uznaje się za udaną przy dowolnym kodzie 2xx. Wszystko inne — łącznie z przekroczeniem
limitu czasu — jest ponawiane z wykładniczo rosnącym odstępem (pierwsze ponowienie po około minucie, każde
kolejne po dwukrotnie dłuższej przerwie — łącznie do sześciu prób), po czym dostawa zostaje oznaczona jako nieudana.
To oznacza, że to samo zdarzenie może dotrzeć więcej niż raz. Traktuj X-Aftertalk-Event-Id jako
klucz idempotencji i pomijaj zdarzenia, które już obsłużono:
if (await alreadyHandled(eventId)) return res.status(200).end();Odpowiadaj szybko — zwróć 2xx, gdy tylko zapiszesz zdarzenie, a właściwe przetwarzanie wykonaj później.
Limit czasu odpowiedzi wynosi 10 sekund, a zbyt wolna odpowiedź jest traktowana jak błąd.
Aftertalk nie podąża za przekierowaniami: zarejestruj docelowy adres URL.
Debugowanie webhooka, który nie działa
Każda próba jest zapisywana wraz z kodem statusu i błędem:
curl "$BASE/v1/projects/$PROJECT_ID/webhooks/$WEBHOOK_ID/deliveries" \
-H "X-API-Key: $AFTERTALK_KEY"Ten sam dziennik znajdziesz w sekcji Konfiguracja → Webhooki w projekcie — wiersz każdego endpointu pokazuje wynik jego ostatniej próby, a przycisk „Historia” otwiera historię dostaw.
Sprawdzaj w tej kolejności:
- Brak jakichkolwiek dostaw — czy webhook jest
activei czy subskrybuje to zdarzenie? Wstrzymany webhook nic nie otrzymuje. PENDING— dostawa czeka na wysłanie lub zaplanowane ponowienie.FAILEDz kodem statusu — Twój endpoint odpowiedział kodem innym niż2xx;responseCodeto kod, który zwrócił.FAILEDbez kodu statusu — w ogóle nie udało się z nim połączyć: DNS, TLS, przekroczenie limitu czasu albo adres URL, który nie wskazuje już na adres publiczny.errorMessagepodaje przyczynę.
Wstrzymaj webhook na czas naprawy odbiornika, zamiast pozwalać, by próby się wyczerpały:
curl -X PATCH "$BASE/v1/projects/$PROJECT_ID/webhooks/$WEBHOOK_ID/toggle" \
-H "X-API-Key: $AFTERTALK_KEY"