Skip to main content

Webhooks posteriores a la llamada

Suscríbete a los eventos call.completed: transcripciones, grabaciones, resultados.

6 min read

Webhooks posteriores a la llamada

Notifica a tu servidor cada vez que termina una llamada de Finn. Recibe la transcripción, la URL de la grabación, el resultado estructurado, el sentimiento y el coste. Es el webhook más importante para sincronizar los datos de llamadas con tu CRM, tus analíticas o tu flujo de operaciones.


Cuándo se dispara

  • Evento: call.completed
  • Se dispara: en los 5 segundos posteriores al fin de la llamada (por cualquier motivo: contestada, perdida, buzón de voz, ocupado, fallida)
  • Orden: no garantizado. Usa created_at y controladores idempotentes; no dependas de un orden estricto.
  • Reintentos: 5 intentos en unos 10 minutos ante 5xx o tiempo de espera agotado. El último intento queda registrado en el log de webhooks del panel.

Registrar una suscripción

Desde el panel

Configuración → Integraciones → Webhooks → Nuevo webhook.

Elige call.completed en la lista de eventos. Pega la URL de tu endpoint. Guarda: Finn muestra el secreto de firma una sola vez. Cópialo en ese momento (no podrás volver a verlo).

Mediante 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 respuesta incluye secret; guárdalo de forma segura.


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

Referencia de campos

CampoTipoNotas
call_idstringÚnico a nivel global. Úsalo como clave de deduplicación.
deployment_idstringCampaña de origen. Null en llamadas de prueba puntuales.
from / toE.164Antes de la traducción. Las llamadas de WhatsApp usan wa_id aquí.
channelenumpstn | whatsapp | sip
started_atISO-8601Cuándo inició Finn la marcación o recibió la llamada
answered_atISO-8601 / nullNull si la llamada nunca se contestó
duration_secondsintDuración facturable, de answered_at a ended_at
outcome.labelstringTu taxonomía de resultados personalizada definida en el prompt
outcome.confidencefloat 0–1Confianza del modelo en la etiqueta
outcome.extracted_fieldsobjectDatos estructurados por llamada (formato libre según el diseño del prompt)
sentimentenumpositive | neutral | negative
call_statusenumcompleted | no_answer | busy | failed | voicemail
hangup_partyenumcaller | agent | system
credits_chargedfloatCargo al monedero por esta llamada
audience_rowobjectLa fila completa del CSV marcada (para llamadas salientes). Null en llamadas entrantes.

Verificación de firma

Finn firma cada webhook con HMAC-SHA256. Verifícala antes de confiar en el payload.

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

El t= es la marca de tiempo. El v1= es HMAC-SHA256(secret, t + "." + raw_body).

Node

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)

Verifica siempre sobre el cuerpo bruto de la petición, no sobre el JSON parseado: al reserializar cambia el espaciado y se rompe el HMAC.


Procesamiento del payload (ejemplo en Node)

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

Fiabilidad e idempotencia

  • Deduplica por event.id: Finn puede reenviar un evento ya entregado si tu endpoint agotó el tiempo de espera
  • Responde 2xx en menos de 5 segundos: de lo contrario lo tratamos como fallo y reintentamos
  • Haz el trabajo pesado de forma asíncrona: encola el payload y confirma de inmediato
  • Reintenta desde el panel: las entregas fallidas aparecen en Configuración → Integraciones → Webhooks → Registros con un botón "Reintentar"

Problemas frecuentes

SíntomaSolución
La firma siempre fallaEstás analizando el JSON antes de verificar. Verifica primero el cuerpo sin procesar.
Filas duplicadas en el CRMNo estás deduplicando por event.id. Añade una restricción de unicidad o un conjunto de vistos.
Eventos tardíosTu endpoint tardó más de 5 s. Mueve el procesamiento a una cola.
Falta audience_rowLa llamada fue entrante (sin audiencia). Comprueba call_type primero.
Error 403 en la URL de grabaciónLas URL de grabación caducan a los 30 días por defecto. Réplicalas en tu propio almacenamiento para archivarlas a largo plazo.

Eventos relacionados

El webhook posterior a la llamada es uno de varios. Consulta el catálogo completo:

EventoCuándo
call.startedEl operador descolgó
call.completedEsta página
call.transferredTransferencia asistida a un agente humano
deployment.completedLa campaña agotó la audiencia
wallet.low_balancePor debajo del umbral configurado

Relacionado

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.