Monitorizar e integrar
Webhook de fin de llamada
Configuración
Crea el webhook
POST
/v1/webhookscon unnamey tuendpointUrl.{ "name": "CRM sync", "endpointUrl": "https://example.com/hooks/callshift?token=YOUR_SECRET" }Asígnalo a un agente
PUT
/v1/agents/{agentId}/webhookcon{"enabled": true, "webhookId": "…"}. Un webhook puede servir a muchos agentes, y un agente duplicado conserva su webhook.
Payload
{
"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
metadatadel contacto está dentro decontact, no en la raíz: mapeacontact.metadata.plan. entityesnullen la llamada de un agente y un número compartidos. Enruta porentity.externalId, tu propio identificador, en lugar de por el nombre.qualificationCriteriausa como claves los nombres de la información que debe recopilar el agente, y sus valores llegan como texto: unbooleanes"true"o"false".callerNumberes el número del cliente, sea cual sea el sentido de la llamada: en una llamada saliente es igual ato. Una llamada desde un navegador lleva"web-call".durationestá en segundos, la misma cifra que elbillingSecondsde la llamada.contactse omite cuando el número no coincide con ningún contacto, yrecordingUrlpuede estar vacío.phoneyqualificationCriteriacoinciden 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
2xxde inmediato y haz el trabajo después, en una cola; - haz que el procesamiento sea idempotente, con
callIdcomo clave; - recupera lo que se te haya escapado listando GET
/v1/callsde 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 afterwardsGestionar los webhooks
/v1/webhooksGET/v1/webhooks/{webhookId}PATCH/v1/webhooks/{webhookId}DELETE/v1/webhooks/{webhookId}Eliminar un webhook detiene las entregas de todos los agentes que lo usan.
