Monitorizar e integrar
Conectar n8n o Make
Cómo funciona
CallShift envía, tu flujo escucha. Cuando termina una llamada, CallShift envía a la URL de tu flujo un cuerpo JSON con todo lo relativo a la llamada (resumen, transcripción, resultado, información recopilada, grabación). Tu flujo no tiene que consultar nada periódicamente.
Construye tu flujo en torno a estas reglas:
| Regla | Valor | Por eso, tu flujo |
|---|---|---|
| Evento | Solo al final de la llamada | No recibe nada cuando empieza una llamada o una transferencia |
| Formato | POST, Content-Type: application/json | Empieza por un disparador webhook que acepte POST |
| Tiempo de respuesta | 10 segundos como máximo | Responde 200 de inmediato y hace el trabajo después |
| Reintentos | Ninguno | Recupera por API, de forma programada, las llamadas que no recibió |
| Firma | Ninguna | Comprueba un token secreto en su URL |
| Asignación | Un webhook por agente | Puede recibir las llamadas de todos tus agentes en una sola URL |
La guía del webhook de fin de llamada detalla cómo se hace la entrega.
Configuración en cuatro pasos
Consigue la URL de tu flujo
La URL de producción de un nodo Webhook de n8n, o la URL de un webhook personalizado de Make (las dos secciones siguientes). Debe ser pública y en HTTPS: CallShift envía desde la nube.
Crea el webhook
En el panel, en app.callshift.ai/webhooks: un nombre (Sincronización CRM n8n) y la URL. O por API, con POST
/v1/webhooks.Asígnalo al agente
En la página del agente, pestaña After-Call Automations: activa End of call event, elige el webhook en Select Webhook endpoint y pulsa Save. O por API, con PUT
/v1/agents/{agentId}/webhook. Hazlo con cada agente; un agente duplicado conserva el webhook del agente del que se copió.Llama al agente
Llama a un número que atienda el agente y comprueba que el flujo ha recibido la llamada (Prueba de principio a fin).
En n8n
Empieza el flujo con un nodo Webhook:
| Ajuste | Valor |
|---|---|
| HTTP Method | POST |
| Path | Conserva la ruta aleatoria que propone n8n: es difícil de adivinar |
| Respond | Immediately, para que CallShift reciba su 200 en menos de 10 segundos, haga lo que haga el flujo después |
- Da a CallShift la URL de producción,
/webhook/..., y activa el flujo. La URL de prueba,/webhook-test/..., solo escucha mientras el editor espera un evento de prueba, y para una sola llamada. - Añade a la URL
?token=y un valor aleatorio largo, y pon un nodo If justo después del webhook:{{ $json.query.token }}debe ser igual a tu token; si no, el flujo se detiene. - La llamada está en
$json.body:{{ $json.body.callId }},{{ $json.body.summary }}. - Para construir el flujo antes de la primera llamada real, pega el payload de ejemplo de más abajo como datos fijados del nodo Webhook (Pin data).
En Make
Empieza el escenario con la app Webhooks, disparador Custom webhook, y crea un webhook: Make te da al instante una URL HTTPS pública.
- La URL es larga y aleatoria: mantenla en secreto, hace las veces de token.
- Para que Make aprenda la estructura de una llamada, haz clic en Redetermine data structure y envíale el payload de ejemplo de más abajo, guardado como
callshift-payload.json:
curl -X POST "https://hook.eu1.make.com/<your-webhook-address>" \
-H "Content-Type: application/json" \
-d @callshift-payload.json- Activa el escenario, con la programación Immediately. Make responde
200en cuanto recibe la petición; si añades un módulo Webhook response, colócalo antes de cualquier módulo lento. - Los campos se mapean como
{{1.callId}},{{1.contact.metadata.plan}}, donde1es el número del módulo webhook.
Hacer accesible un n8n autoalojado
Una URL de Make y una URL de n8n Cloud funcionan tal cual. Un n8n autoalojado suele responder en una dirección interna que CallShift, desde la nube, no puede alcanzar:
| En la URL | Qué falla | Qué hacer |
|---|---|---|
n8n.company.local | Un nombre que solo conoce tu red | Publica un nombre DNS público, por ejemplo un subdominio para los webhooks |
:5678 | El puerto propio de n8n, cerrado al exterior | Pon n8n detrás de un proxy inverso en el puerto 443 |
http:// | Sin cifrar, y el payload lleva transcripciones y números de teléfono | HTTPS con un certificado válido |
/webhook-test/ | Solo escucha durante una prueba en el editor | La URL de producción, /webhook/, con el flujo activo |
Hay tres formas habituales de exponerlo, según tu política de red: un proxy inverso que ya tengas en marcha (nginx, Traefik, HAProxy, Caddy), un túnel saliente que no abre ningún puerto de entrada (como Cloudflare Tunnel) o n8n Cloud. Expón solo la ruta /webhook/, no el editor, y define la variable WEBHOOK_URL de n8n con la URL pública. Si tu política exige una lista de direcciones IP de origen, escribe a support@callshift.ai.
Comprueba la URL tal como la ve CallShift: desde un equipo fuera de tu red y sin VPN, por ejemplo compartiendo la conexión de un móvil. El nombre debe resolverse en una dirección IP pública:
nslookup n8n.example.com 8.8.8.8Y la URL debe responder a un POST en menos de 10 segundos:
curl -i -X POST "https://n8n.example.com/webhook/<path>?token=<your-token>" \
-H "Content-Type: application/json" \
-d '{"test": true}' \
-w "\nTotal time: %{time_total} s\n"| Resultado | Significado | Qué hacer |
|---|---|---|
200, una ejecución en n8n, en menos de 10 s | Todo listo | Pruébalo de principio a fin |
Could not resolve host | El nombre no es público | Publica el nombre DNS |
Tiempo de espera agotado, Connection refused | Puerto cerrado o firewall | Abre el puerto 443 y redirígelo al proxy inverso |
SSL certificate problem | Certificado no válido o autofirmado | Un certificado reconocido, como Let's Encrypt |
404, webhook no registrado | Flujo inactivo o URL de prueba | Activa el flujo y usa /webhook/ |
| Más de 10 s | El flujo responde al terminar | Configura el nodo Webhook para que responda Immediately |
Los campos de una llamada
Un ejemplo de lo que recibe tu flujo. Los valores son inventados: comprueba la estructura con tu primera llamada real.
{
"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" }
}
}| Campo | Tipo | Cuándo | Qué contiene |
|---|---|---|---|
callId | UUID | Siempre | El id de la llamada: la clave para eliminar duplicados y recuperar llamadas |
type | texto | Siempre | inbound, outbound o webcall |
status | texto | Siempre | COMPLETED para una llamada que terminó con normalidad |
createdAt, startedAt, endedAt | fecha ISO 8601, UTC | createdAt siempre; las otras dos, null si se desconocen | Cuándo se creó, empezó y terminó la llamada |
duration | entero | Cuando la llamada tiene sus fechas | Segundos, la misma cifra que el billingSeconds de la llamada |
from, to | texto | Casi siempre | Saliente: tu línea y luego el cliente. Entrante: el cliente y luego tu línea |
callerNumber | texto | Siempre | El número del cliente, sea cual sea el sentido de la llamada; "web-call" para una llamada desde un navegador |
campaignId | UUID o null | Llamadas de campaña | La campaña que hizo la llamada |
endedReason | texto | Cuando la llamada tiene uno | Por qué terminó: consulta Motivos de fin de llamada |
isReached, isSignificant, isSilent, isSuccessful | booleano | Una vez analizada la llamada | El resultado: consulta Análisis de llamadas |
summary | texto | Una vez analizada la llamada | El resumen, vacío cuando nadie habló |
transcript | texto o null | Cuando se dijo algo | Una línea por turno, que empieza por User: o Assistant: |
recordingUrl | URL o null | Agentes con el motor de voz actual | Un enlace directo a la grabación; puede estar vacío |
agent | {id, name} | Siempre | El agente de la llamada |
phone | {id, number} o null | Siempre | Tu línea de CallShift |
entity | {id, externalId, name} o null | Siempre | La entidad de la llamada, null si es compartida: consulta Entidades |
qualificationCriteria | objeto | Una vez analizada la llamada | Una clave por criterio del agente, con su valor como texto |
contact | objeto | Cuando el número coincide con un contacto | id, phoneNumber, firstName, lastName, email, information, metadata |
Un campo sin valor no aparece en el cuerpo: mapéalo como opcional.
Cuatro errores frecuentes al mapear
- Las claves de los criterios son sus nombres exactos, con tildes y mayúsculas incluidas, y cada agente tiene los suyos. Cópialos de los ajustes del agente y considera un nombre como fijo en cuanto un flujo lo lea: cambiarlo deja vacío el mapeo sin que salte ningún error.
- Los valores de los criterios son texto. Un sí o un no llega como
"true"o"false": compáralo con el texto, nunca con un booleano. En n8n:{{ $json.body.qualificationCriteria["Visit booked"] === "true" }}; en Make, un filtro con el operador de texto Equal totrue. - Los metadatos están dentro del contacto:
contact.metadata.plan, nometadata.plan. Una ruta incorrecta devuelve un valor vacío, sin que salte ningún error. - Las fechas están en UTC. Conviértelas a tu zona horaria antes de escribirlas en cualquier sitio que lea una persona. En n8n:
{{ DateTime.fromISO($json.body.endedAt).setZone("Europe/Paris").toFormat("yyyy-MM-dd HH:mm") }}; en Make,formatDatecon la zona horaria.
Prueba de principio a fin
No hace falta ningún simulador: una llamada real a un número que atiende el agente envía el webhook unos segundos después de colgar (lo que se tarda en redactar el resumen y extraer la información). Antes de llamar, comprueba que la URL responde (con las comprobaciones de más arriba), que el flujo está activo en su URL de producción y que el webhook está asignado al agente.
| Llamada que hacer | Qué debe recibir el flujo |
|---|---|
| Una conversación completa, hasta el objetivo del agente | isSuccessful en true, un summary claro, todos los criterios esperados |
| Una llamada que el agente deba transferir, si transfiere llamadas | endedReason en assistant-forwarded-call |
| Cuelga a los dos segundos | isSignificant en false, una transcripción casi vacía: el flujo no debe fallar con ella |
Luego comprueba que:
callIdestá presente y cambia en cada llamada;callerNumberes el número desde el que llamaste;transcriptysummarycorresponden a la conversación;- están todos los criterios que lee tu flujo, con el nombre que mapeaste;
recordingUrlabre la grabación;- las fechas aparecen en tu zona horaria en tus herramientas.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Ninguna ejecución en n8n o Make | URL inaccesible o flujo inactivo | Repite las comprobaciones de la URL de más arriba |
| Sigue sin llegar nada | Webhook no asignado a este agente, o no guardado | Revisa la pestaña After-Call Automations del agente y, después, escribe a support@callshift.ai con la hora de la llamada |
| Una ejecución que falla | Un mapeo con una ruta incorrecta | Compáralo con el cuerpo en bruto que recibió la ejecución |
Cada entrega, respondida o fallida, aparece también en los registros del panel, con la respuesta de tu flujo.
Recuperar las llamadas no entregadas
Una entrega fallida no se reenvía. Un segundo flujo, programado (cada 30 minutos, cada hora), vuelve a consultar las llamadas por API y añade las que tu destino no tiene:
- un nodo Schedule Trigger en n8n, o un escenario programado en Make;
- una petición HTTP a GET
/v1/calls, filtrada por agente y por periodo (fromyto, fechas ISO completas aplicadas a la fecha de creación), con tu clave de API en una cabeceraAuthorization: Bearerguardada en las credenciales de la herramienta, y leyendo las páginas hastapageCount(consulta Paginación); - una comprobación de cada
callIdfrente a lo que ya contiene tu destino, para añadir solo las que faltan; - para la transcripción completa de una llamada, GET
/v1/calls/{callId}.
curl "https://api.callshift.ai/v1/calls?agentId=$AGENT_ID&from=2026-10-30T08:00:00Z&to=2026-10-30T09:00:00Z&page=1&limit=100" \
-H "Authorization: Bearer $CALLSHIFT_API_KEY"Crea una clave solo para este flujo en app.callshift.ai/api-keys y guárdala en las credenciales de la herramienta, nunca en claro en un flujo que compartas.
Enviar contactos a una campaña
En el sentido contrario: tu flujo añade contactos a una campaña, que los llama. Envíalos juntos, en una sola petición a POST/v1/campaigns/{campaignId}/contacts, en lugar de una petición por contacto.
- Normaliza los números a E.164 (
+33612345678) antes de enviarlos: CallShift identifica los contactos por número y no los normaliza por ti. - Omite el email si no estás seguro de él. Un email presente pero vacío o mal formado hace que se rechace toda la petición con un
400; guarda los valores dudosos enmetadata, que acepta cualquier JSON. - Una campaña activa marca de inmediato. Haz primero una prueba con uno o dos números internos.
En n8n, un nodo Code puede preparar cada contacto, aquí para números franceses:
// Mode "Run Once for Each Item": one contact per item, French numbers to E.164.
let phone = String($json.phone ?? "").replace(/[^\d+]/g, "");
if (phone.startsWith("00")) phone = "+" + phone.slice(2);
else if (phone.startsWith("0")) phone = "+33" + phone.slice(1);
const email = String($json.email ?? "").trim();
const validEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
return {
json: {
phoneNumber: phone,
firstName: $json.firstName,
lastName: $json.lastName,
...(validEmail ? { email } : {}),
metadata: { source: "n8n", ...(email && !validEmail ? { rawEmail: email } : {}) },
},
};Después, un nodo Aggregate los reúne en una sola lista contacts, y un nodo HTTP Request la envía:
curl -X POST "https://api.callshift.ai/v1/campaigns/$CAMPAIGN_ID/contacts" \
-H "Authorization: Bearer $CALLSHIFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{ "phoneNumber": "+33612345678", "firstName": "Ada", "metadata": { "source": "n8n" } },
{ "phoneNumber": "+33687654321", "firstName": "Louis", "email": "louis@example.com", "metadata": { "source": "n8n" } }
]
}'Pedir a un asistente de IA que construya el flujo
Tres prompts para pegar en Claude, ChatGPT o Gemini. Cada uno incluye lo que el asistente necesita saber de CallShift: sustituye lo que va entre corchetes.
Construir el flujo que recibe las llamadas
Eres un experto en [n8n / Make]. Ayúdame a construir un flujo que reciba los webhooks de fin de llamada de la plataforma de voz CallShift y los envíe a [DESTINO: nuestro CRM, una hoja de Google Sheets, nuestra herramienta de tickets].
Las restricciones de CallShift:
- Un POST JSON por llamada terminada, enviado una sola vez, sin reintentos, con un timeout de 10 segundos.
- Sin firma: la URL lleva un parámetro ?token= que el flujo debe comprobar primero para rechazar la petición si falta o es incorrecto.
- Responder 200 de inmediato y hacer el trabajo después.
- Eliminar duplicados por callId.
El cuerpo: en la raíz callId, type, status, createdAt, startedAt, endedAt (ISO 8601 en UTC), duration (segundos), from, to, callerNumber, campaignId, endedReason, isReached, isSignificant, isSilent, isSuccessful, summary, transcript, recordingUrl, agent {id, name}, phone {id, number}, entity {id, externalId, name} o null. qualificationCriteria es un objeto plano cuyas claves son los nombres exactos de los criterios del agente y cuyos valores son texto ("true" o "false" para un sí o un no). contact, ausente cuando el número no coincide con ningún contacto, contiene id, phoneNumber, firstName, lastName, email, information y metadata. Un campo sin valor se omite. transcript es un solo bloque de texto, con una línea por turno que empieza por "User:" o "Assistant:".
Este es un cuerpo que recibió mi flujo: [PEGA EL JSON EN BRUTO]
Dame los nodos o módulos en orden, los ajustes de cada uno, la expresión exacta de cada campo mapeado, la conversión de las fechas a [ZONA HORARIA] y cómo tratar las llamadas muy cortas (isSignificant false).Hacer accesible un n8n autoalojado
Eres un ingeniero de redes. Nuestra instancia de n8n responde en una dirección interna, [DIRECCIÓN INTERNA, por ejemplo http://n8n.company.local:5678]. Una plataforma SaaS en la nube (CallShift) debe enviarle peticiones POST HTTPS desde Internet, en la ruta /webhook/<id>. Envía cada petición una sola vez y espera una respuesta en menos de 10 segundos.
Nuestro contexto: [FIREWALL, DMZ, PROXY INVERSO EXISTENTE, CÓMO SE ALOJA N8N (Docker, VM), RESTRICCIONES DE NUESTRO EQUIPO DE TI].
Propón la forma más sencilla y segura de exponer SOLO la ruta /webhook/ de n8n (no el editor), en HTTPS en el puerto 443 con un certificado válido. Compara un proxy inverso en una DMZ, un túnel saliente y n8n Cloud. Dame la configuración exacta de la opción que recomiendes, las variables de entorno de n8n que hay que definir (WEBHOOK_URL en particular) y los comandos nslookup y curl para comprobarlo desde fuera de nuestra red.Recuperar las llamadas no entregadas
Eres un experto en [n8n / Make]. El webhook de fin de llamada de CallShift nunca se reintenta: cuando nuestro endpoint está caído, el webhook de esa llamada se pierde. Construye un flujo programado, cada [30 MINUTOS / HORA], que:
1. Haga una petición GET https://api.callshift.ai/v1/calls con la cabecera Authorization: Bearer <clave de API guardada en las credenciales de la herramienta>, filtrando por agentId y por periodo (from y to, fechas ISO 8601 completas, aplicadas a la fecha de creación).
2. Lea todas las páginas: la consulta acepta page (desde 1) y limit (hasta 100), y la respuesta devuelve data y pageCount.
3. Compare el callId de cada llamada con los que ya están en [DESTINO] y añada solo los que falten.
4. Para una llamada, GET https://api.callshift.ai/v1/calls/{callId} devuelve el detalle completo, transcripción incluida.
Dame los nodos o módulos, sus ajustes y las expresiones exactas.