Skip to main content

Webhooks efter samtal

Prenumerera på call.completed-händelser — transkriptioner, inspelningar, utfall.

5 min read

Post-call Webhooks

Brand din server varje gång ett Finn samtal slutar. Få transkript, inspelning URL, strukturerat resultat, sentiment och kostnad. Den enskilt viktigaste webhook för att synkronisera samtalsdata till din CRM, analys eller ops pipeline.


När det eldar

  • Event: call.completed
  • ** Brand: ** inom 5 sekunder efter samtalet (någon anledning – plockad, missad, röstbrevlåda, upptagen, misslyckad)
  • Order: ** Best-effort. Använd created_at + idempotenta hanterare, lita inte på strikt ordning.
  • *Retries: 5 försök över ~ 10 minuter på 5xx / timeout. Sista försöket inloggat i instrumentbrädan webhook log.

Registrera en prenumeration

Via Dashboard

** Inställningar → Integrationer → Webhooks → Ny webhook****.

Välj call.completed från evenemangslistan. Klistra din endpoint URL. Spara - Finn visar undertecknande hemligheten en gång. Kopiera det nu (du kan inte se det igen).

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

Response inkluderar secret – lagra säkert.


Payload

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

Fält referens

| Fält | Typ | Anteckningar | |---------- | call_id | sträng | globalt unik. Använd som din dedupe key. | | deployment_id | sträng | Källa kampanj. Null för engångs testsamtal. | | from/ to | E.164 | Föröversättning. WhatsApp kallar wa_id här | channel | enum | pstn whatsapp sip | | started_at | ISO-8601 | När Finn inledde uppringning / mottog samtalet | | answered_at | ISO-8601 / null | Null om samtalet aldrig plockade upp | | duration_seconds | int | Förenlig varaktighet, från answered_at till ended_at | | outcome.label | sträng | Din anpassade resultat taxonomi från prompten | | outcome.confidence | flotta 0–1 | Modellförtroende i etiketten | | outcome.extracted_fields | objekt | Per-call strukturerade data (fri form genom snabb design) | | sentiment | enum | positive neutral negative | | call_status | enum | completed no_answer busy failed õ | | 5 | | hangup_party | enum | caller agent system | | credits_charged | flyt | Wallet debit för detta samtal | | audience_row | objekt | Den fullständiga CSV-raden ringde (för utgående). Null för inbound


Signaturverifiering

Finn undertecknar varje webhook med HMAC-SHA256. Verifiera innan du litar på nyttolast.

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

t= är tidsstämpeln. v1= är HMAC-SHA256(secret, t + "." + raw_body).

Nod

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)

** Kontrollera alltid på den råa förfrågan kroppen*, inte den parsed JSON - om-serialisering ändrar vitrymden och bryter HMAC.


Hantera nyttolast (exempelvis)

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

Tillförlitlighet + idempotens

  • ** Dedupe by event.id*****************************************************************************************************************************************************************************************************************************************************
  • *Retur 2xx inom 5 sekunder - annars behandlar vi det som ett misslyckande och fördröjning
  • ** Gör det tunga arbetet asynk** -- kö nyttolast, ack omedelbart
  • *Replay från instrumentbrädan - misslyckade leveranser visas i ** Inställningar → Integrationer → Webhooks → Logs ** med en "Replay" -knapp

Vanliga gotchas

Symptom | Fix | |-------- Signatur misslyckas alltid | Du parsar JSON innan du verifierar. Verifiera rå kropp först. | | Duplicera CRM rader | Att inte dras av event.id. Lägg till en unik begränsning eller synlig. | Sena händelser | Din slutpunkt tog > 5s. Flytta bearbetning till en kö. | | Saknade audience_row | Samtalet var inkommande (ingen publik). Kontrollera call_type första | Inspelning URL 403 | Inspelningsadresser löper ut efter 30 dagar som standard. Spegel till ditt eget lagringsutrymme för långsiktigt arkiv


Relaterade händelser

Post-call webhook är en av flera. Se hela katalogen:

Event | När | |-------- | call.started | Carrier plockade upp | | call.completed | Denna sida | | call.transferred | Varm överföring till människa | | deployment.completed | Kampanjutmattad publik | | wallet.low_balance | Nedan konfigurerad tröskel


Relaterad

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.