Skip to main content

Webhooks na het gesprek

Abonneer je op call.completed-events — transcripties, opnames, uitkomsten.

4 min read

Post-call Webhooks

Brand uw server elke keer als een Finn gesprek eindigt. Ontvang transcriptie, opname URL, gestructureerde uitkomst, sentiment, en kosten. De belangrijkste webhook voor het synchroniseren van call data in uw CRM, analytics, of ops pipeline.


Als het vuurt

  • Event: call.completed
  • Bestanden: binnen 5 seconden na het einde van de oproep (om het even welke reden gekozen, gemist, voicemail, bezet, mislukt)
    • Order:** best-forfort. Gebruik created_at + idempotent handlers, vertrouw niet op strikte volgorde.
  • Retries: 5 pogingen over ~10 minuten op 5xx / timeout. Laatste poging aangemeld in het dashboard webhook log.

Een abonnement registreren

Via dashboard

Instellingen → Integraties → Webhooks → Nieuwe webhook.

Kies call.completed uit de evenementenlijst. Plak je eindpunt URL. Opslaan Finn toont het ondertekening geheim eenmaal. Kopieer het nu (je kunt het niet meer zien).

Via 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"
  }'

De respons omvat secret .


Betaling

{
  "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"
    }
  }
}

Veldreferentie

Veld Type Notities


De string is wereldwijd uniek. Gebruik als uw dedupe sleutel Bron campagne. Null voor eenmalige testoproepen Voorvertaling. WhatsApp calls gebruiken wa_id hier #channel ISO-8601 Toen Finn gestart met het bellen / ontvangen van de oproep | answered_at | ISO-8601 / null | Null if the call never picked up | | duration_seconds | int | Billable duration, from answered_at to ended_at | Uw aangepaste uitkomst taxonomie van de prompt | outcome.confidence | float 0–1 | Model confidence in the label | | outcome.extracted_fields | object | Per-call structured data (free-form by prompt design) | #sentiment | call_status | enum | completed | no_answer | busy | failed | voicemail | #hangup_party Float Wallet debit voor deze oproep De volledige CSV-rij (voor uitgaande). Null voor inkomend


Controle van de handtekening

Finn tekent elke webhaak met HMAC-SHA256. Controleer voordat je de lading vertrouwt.

Kop: X-Finn-Signature: t=1716391823,v1=abc123...

De t= is het tijdstempel. De v1= is HMAC-SHA256(secret, t + "." + raw_body).

Knooppunt

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)

** Controleer altijd op de ruwe aanvraag-lichaam**, niet de ontleed JSON re-serialization verandert witruimte en breekt de HMAC.


Omgaan met de lading (Node voorbeeld)

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

Betrouwbaarheid + geschiktheid

  • Dedupe door event.id
  • Terugkeer 2xx binnen 5 seconden
  • Doe het zware werk async
  • Replay vanaf dashboard Faalde leveringen verschijnen in Settings → Integraties → Webhooks → Logs met een "Replay" knop

Gemeenschappelijke gotcha's

Symptoom Fix |---|---| Je ontleedt JSON voordat je het controleert. Controleer eerst het ruwe lichaam Voeg een unieke beperking of seen-set toe Je eindpunt nam > 5s. Verplaats verwerking naar een wachtrij De oproep was inkomende (geen publiek). Controleer call_type eerst Opname van URL-URL-NL-adressen verlopen standaard na 30 dagen. Spiegel naar uw eigen opslag voor lange termijn archief


Gerelateerde gebeurtenissen

De post-call webhook is een van de vele. Zie de volledige catalogus:

Gebeurtenis Wanneer |---|---| Draagtuig opgepikt Deze pagina Warm overbrengen naar de mens De campagne is uitgeput Onder geconfigureerde drempel


Gerelateerd

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.