Skip to main content

Webhooks post-appel

Abonnez-vous aux événements call.completed — transcriptions, enregistrements, résultats.

5 min read

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 dupe deployment_id="string=" Campagne source. Null pour les appels de test uniques from / to= E.164= Pré-traduction. Les appels WhatsApp utilisent wa_id ici 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, du answered_at au ended_at Votre taxonomie de résultat personnalisé à partir de l'invite outcome.confidence=2 flotter 0–1=2 confiance dans l'étiquette outcome.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_type en 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.started Cette page call.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.