Webhooks post-appel
Abonnez-vous aux événements call.completed — transcriptions, enregistrements, résultats.
Après appel Webhooks
Lancez votre serveur chaque fois qu'un appel Finn se termine. Recevoir la transcription, enregistrer l'URL, le résultat structuré, le sentiment et le coût. Le webhook le plus important pour synchroniser les données d'appel dans votre pipeline CRM, analytique ou ops.
Quand il tire
- Événement:
call.completed - ** Feu:** dans les 5 secondes suivant la fin de l'appel (toute raison — choisi, manqué, messagerie vocale, occupé, échoué)
- Ordre : meilleur effort. Utilisez
created_at+ gestionnaires idémpotent, ne vous fiez pas à l'ordre strict. - Retries: 5 tentatives sur ~10 minutes sur
5xx/ timeout. Dernière tentative enregistrée dans le journal de bord webhook.
Inscrivez un abonnement
Par le tableau de bord
Paramètres → Intégrations → Webhooks → Nouveau webhook.
Choisissez call.completed dans la liste des événements. Coller l'URL de votre terminal. Enregistrer — Finn montre le secret de signature une fois. Copiez-le maintenant (vous ne pouvez pas le revoir).
Par API
curl https://api.hirefinn.ai/v1/webhooks \
-H "Authorization: Bearer $FINN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/finn/post-call",
"events": ["call.completed"],
"description": "CRM + analytics sync"
}'
La réponse comprend secret — stocker en toute sécurité.
Charge utile
{
"id": "evt_2H4abc",
"type": "call.completed",
"created_at": "2026-05-22T14:32:14.892Z",
"org_id": "org_8fac17c5",
"data": {
"call_id": "cal_xyz789",
"deployment_id": "dep_abc123",
"finn_id": "fn_def456",
"phone_number_id": "ph_1234",
"from": "+919876543210",
"to": "+918765432100",
"call_type": "outbound",
"channel": "pstn",
"started_at": "2026-05-22T14:30:01.245Z",
"answered_at": "2026-05-22T14:30:04.812Z",
"ended_at": "2026-05-22T14:32:14.123Z",
"duration_seconds": 130,
"ring_seconds": 4,
"outcome": {
"label": "qualified",
"confidence": 0.87,
"extracted_fields": {
"preferred_slot": "2026-05-24T15:00:00+05:30",
"budget": "25000",
"is_decision_maker": true
}
},
"sentiment": "positive",
"csat_score": null,
"call_status": "completed",
"hangup_party": "agent",
"recording_url": "https://recordings.hirefinn.ai/.../cal_xyz789.mp3",
"recording_duration_seconds": 130,
"transcript_url": "https://transcripts.hirefinn.ai/.../cal_xyz789.json",
"credits_charged": 3,
"currency": "INR",
"monetary_value": 10.05,
"audience_id": "aud_xyz789",
"audience_row": {
"name": "Priya M",
"phone": "+919876543210",
"loan_amount": "500000"
},
"metadata": {
"campaign_tag": "may-cohort-3"
}
}
}
Référence du champ
Champ Type Remarques
- Oui
call_id=" chaîne de caractères" Globally unique. Utilisez comme clé de dupedeployment_id="string=" Campagne source. Null pour les appels de test uniquesfrom/to= E.164= Pré-traduction. Les appels WhatsApp utilisentwa_idici le nombre d'heures travaillées dans le cadre d'un programme d'études est le suivant : ISO-8601 Quand Finn a commencé à composer / a reçu l'appel ISO-8601 / null (Null) Si l'appel n'a jamais été reçu (Null) Durée de facturation, duanswered_atauended_atVotre taxonomie de résultat personnalisé à partir de l'inviteoutcome.confidence=2 flotter 0–1=2 confiance dans l'étiquetteoutcome.extracted_fields=" objet=" Données structurées par appel (forme libre par conception rapide)=" le nombre d'heures travaillées dans le cadre d'un programme d'études est le suivant : le nombre d'heures de travail est le nombre d'heures travaillées le nombre d'heures travaillées dans le cadre d'un programme d'études est le suivant :credits_charged="Flott" Débit du portefeuille pour cet appel La ligne CSV pleine et composée (pour l'aller). Null pour l'arrivée
Vérification de la signature
Finn signe chaque webhook avec HMAC-SHA256. Vérifiez avant de faire confiance à la charge utile.
En-tête: X-Finn-Signature: t=1716391823,v1=abc123...
Le t= est l'horodatage. Le v1= est HMAC-SHA256(secret, t + "." + raw_body).
Noeud
import crypto from "crypto";
function verifyFinnSignature(
rawBody: string,
header: string,
secret: string,
toleranceSeconds = 300,
): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string]),
);
const ts = parseInt(parts.t, 10);
const sig = parts.v1;
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false; // replay guard
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Python
import hmac, hashlib, time
def verify_finn_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts.get("t", 0))
sig = parts.get("v1", "")
if not ts or not sig: return False
if abs(time.time() - ts) > tolerance: return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Vérifiez toujours sur le corps de requête brute, et non sur le JSON analysé — la ré-sérialisation change l'espace blanc et brise le HMAC.
Traitement de la charge utile (exemple de nœud)
import express from "express";
const app = express();
// raw body required for signature verification
app.post("/finn/post-call",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["x-finn-signature"] as string;
const ok = verifyFinnSignature(req.body.toString(), sig, process.env.FINN_WEBHOOK_SECRET!);
if (!ok) return res.status(401).send("bad signature");
const event = JSON.parse(req.body.toString());
// Dedupe — idempotent processing
if (await db.eventExists(event.id)) {
return res.status(200).send("dup");
}
await db.markEventSeen(event.id);
// Route by event type
if (event.type === "call.completed") {
await handleCallCompleted(event.data);
}
res.status(200).send("ok");
},
);
async function handleCallCompleted(call: any) {
// 1. Update CRM record
await crm.updateLead(call.audience_row?.phone, {
last_call_outcome: call.outcome.label,
last_call_sentiment: call.sentiment,
last_call_recording: call.recording_url,
});
// 2. If qualified, fire a Slack alert
if (call.outcome.label === "qualified") {
await slack.notify("#sales-hot-leads", `Hot lead: ${call.audience_row.name}`);
}
// 3. Push to data warehouse
await warehouse.insert("finn_calls", call);
}
Fiabilité + idempotency
- Dedupe par
event.id— Finn peut réessayer un événement que nous avons déjà livré si votre point d'arrivée est dépassé - Retour 2xx dans les 5 secondes — sinon nous le traitons comme un échec et une réessayer
- Faites le travail lourd async — file d'attente de la charge utile, ack immédiatement
- Replay à partir du tableau de bord — livraisons ratées apparaissent dans Paramètres → Intégrations → Webhooks → Logs avec un bouton "Replay"
Gotchas communs
Symptômes
- Oui
La signature échoue toujours. Vérifier d'abord le corps brut
Dupliquer CRM rows. Ajoutez une contrainte unique ou un ensemble visible
Votre résultat a pris > 5s. Déplacer le traitement vers une file d'attente
L'appel a été reçu (pas d'audience). Vérifiez
call_typeen premier Enregistrer l'URL 403L'enregistrement des URL expire après 30 jours par défaut. Miroir à votre propre stockage pour les archives à long terme
Manifestations connexes
Le webhook post-appel est l'un des nombreux. Voir le catalogue complet :
Événement Quand
- Oui
call.startedCette pagecall.transferred=Le transfert chaud vers l'humain La campagne a épuisé le public Au-dessous du seuil configuré
Autres
Was this page helpful?
Still stuck or have feedback?
Email [email protected] or use the chat bubble in the bottom-right corner — it's a Finn that knows the Academy cold.