Explorar la documentación

Monitorizar e integrar

Webhook de fin de llamada

Cuando termina una llamada, CallShift envía su resultado a tu endpoint: quién llamó, qué se dijo, qué recopiló el agente y cómo terminó la llamada. Conéctalo a tu CRM o a un flujo de n8n o Make.

Configuración

  1. Crea el webhook

    POST/v1/webhooks con un name y tu endpointUrl.

    JSON
    { "name": "CRM sync", "endpointUrl": "https://example.com/hooks/callshift?token=YOUR_SECRET" }
  2. Asígnalo a un agente

    PUT/v1/agents/{agentId}/webhook con {"enabled": true, "webhookId": "…"}. Un webhook puede servir a muchos agentes, y un agente duplicado conserva su webhook.

Payload

JSON
{
  "callId": "8f14e45f-ceea-467a-9575-6d3c0a5f2b1e",
  "type": "outbound",
  "status": "COMPLETED",
  "createdAt": "2026-10-30T09:11:58.000Z",
  "startedAt": "2026-10-30T09:12:00.000Z",
  "endedAt": "2026-10-30T09:13:24.000Z",
  "duration": 84,
  "from": "+33187654321",
  "to": "+33612345678",
  "callerNumber": "+33612345678",
  "campaignId": "3b241101-e2bb-4255-8caf-4136c566a962",
  "endedReason": "customer-ended-call",
  "isReached": true,
  "isSignificant": true,
  "isSuccessful": true,
  "isSilent": false,
  "summary": "Ada booked a visit for Saturday at 10am.",
  "transcript": "Assistant: Hello, this is Alice...\nUser: Hi, I'd like to book a visit...",
  "recordingUrl": "https://recordings.example.com/recordings/8f14e45f.ogg",
  "agent": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Alice" },
  "phone": { "id": "0d3a7e6b-2b8e-4c5b-9c4e-5f4b9d2a1c8e", "number": "+33187654321" },
  "entity": { "id": "9b2d3c4e-5f60-4a1b-8c2d-3e4f5a6b7c8d", "externalId": "club-42", "name": "Lyon Part-Dieu" },
  "qualificationCriteria": { "Visit booked": "true", "Preferred day": "Weekend" },
  "contact": {
    "id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "email": "ada@example.com",
    "phoneNumber": "+33612345678",
    "information": "VIP customer",
    "metadata": { "plan": "pro" }
  }
}
  • El metadata del contacto está dentro de contact, no en la raíz: mapea contact.metadata.plan.
  • entity es null en la llamada de un agente y un número compartidos. Enruta por entity.externalId, tu propio identificador, en lugar de por el nombre.
  • qualificationCriteria usa como claves los nombres de la información que debe recopilar el agente, y sus valores llegan como texto: un boolean es "true" o "false".
  • callerNumber es el número del cliente, sea cual sea el sentido de la llamada: en una llamada saliente es igual a to. Una llamada desde un navegador lleva "web-call".
  • duration está en segundos, la misma cifra que el billingSeconds de la llamada.
  • contact se omite cuando el número no coincide con ningún contacto, y recordingUrl puede estar vacío.
  • phone y qualificationCriteria coinciden con los mismos campos de GET/v1/calls/{callId}.

Entrega

CallShift envía un solo POST con un cuerpo JSON cuando termina la llamada, espera una respuesta durante 10 segundos y no reintenta. Por eso:

  • responde 2xx de inmediato y haz el trabajo después, en una cola;
  • haz que el procesamiento sea idempotente, con callId como clave;
  • recupera lo que se te haya escapado listando GET/v1/calls de forma programada.

Cada entrega, respondida o fallida, aparece en los registros del panel con la respuesta de tu endpoint. No se envía ningún webhook para una llamada que nunca empezó: llamadas entrantes rechazadas (REJECTED) y llamadas de campaña que no se marcaron.

Proteger tu endpoint

Las entregas aún no van firmadas. Pon un token aleatorio largo en la URL del endpoint y rechaza las peticiones que no lo lleven. Antes de actuar a partir de un payload importante (un reembolso, una reserva), vuelve a consultar la llamada con GET/v1/calls/{callId} y tu clave de API.

import express from "express";

const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/hooks/callshift", (req, res) => {
  if (req.query.token !== process.env.CALLSHIFT_HOOK_TOKEN) return res.sendStatus(401);
  res.sendStatus(204); // answer within 10 seconds
  queue.add("callshift-call", req.body, { jobId: req.body.callId }); // idempotent on callId
});

app.listen(3000);
import os
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request

app = FastAPI()

@app.post("/hooks/callshift", status_code=204)
async def callshift_hook(request: Request, tasks: BackgroundTasks, token: str = ""):
    if token != os.environ["CALLSHIFT_HOOK_TOKEN"]:
        raise HTTPException(status_code=401)
    payload = await request.json()
    tasks.add_task(process_call, payload)  # answer first, work afterwards

Gestionar los webhooks

Eliminar un webhook detiene las entregas de todos los agentes que lo usan.