Explorar la documentación

Monitorizar e integrar

Conectar n8n o Make

La mayoría de las integraciones no necesitan código por tu parte: CallShift envía cada llamada a un flujo de n8n o Make, que la registra en tu CRM, en una hoja de cálculo o en tu herramienta de tickets. Esta guía lleva un flujo desde su primer nodo hasta una integración probada, y luego en sentido contrario: de tus herramientas a una campaña.

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:

ReglaValorPor eso, tu flujo
EventoSolo al final de la llamadaNo recibe nada cuando empieza una llamada o una transferencia
FormatoPOST, Content-Type: application/jsonEmpieza por un disparador webhook que acepte POST
Tiempo de respuesta10 segundos como máximoResponde 200 de inmediato y hace el trabajo después
ReintentosNingunoRecupera por API, de forma programada, las llamadas que no recibió
FirmaNingunaComprueba un token secreto en su URL
AsignaciónUn webhook por agentePuede 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

  1. 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.

  2. 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.

  3. 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ó.

  4. 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:

AjusteValor
HTTP MethodPOST
PathConserva la ruta aleatoria que propone n8n: es difícil de adivinar
RespondImmediately, 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:
Terminal
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 200 en 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}}, donde 1 es 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 URLQué fallaQué hacer
n8n.company.localUn nombre que solo conoce tu redPublica un nombre DNS público, por ejemplo un subdominio para los webhooks
:5678El puerto propio de n8n, cerrado al exteriorPon n8n detrás de un proxy inverso en el puerto 443
http://Sin cifrar, y el payload lleva transcripciones y números de teléfonoHTTPS con un certificado válido
/webhook-test/Solo escucha durante una prueba en el editorLa 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:

Terminal
nslookup n8n.example.com 8.8.8.8

Y la URL debe responder a un POST en menos de 10 segundos:

Terminal
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"
ResultadoSignificadoQué hacer
200, una ejecución en n8n, en menos de 10 sTodo listoPruébalo de principio a fin
Could not resolve hostEl nombre no es públicoPublica el nombre DNS
Tiempo de espera agotado, Connection refusedPuerto cerrado o firewallAbre el puerto 443 y redirígelo al proxy inverso
SSL certificate problemCertificado no válido o autofirmadoUn certificado reconocido, como Let's Encrypt
404, webhook no registradoFlujo inactivo o URL de pruebaActiva el flujo y usa /webhook/
Más de 10 sEl flujo responde al terminarConfigura 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.

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" }
  }
}
CampoTipoCuándoQué contiene
callIdUUIDSiempreEl id de la llamada: la clave para eliminar duplicados y recuperar llamadas
typetextoSiempreinbound, outbound o webcall
statustextoSiempreCOMPLETED para una llamada que terminó con normalidad
createdAt, startedAt, endedAtfecha ISO 8601, UTCcreatedAt siempre; las otras dos, null si se desconocenCuándo se creó, empezó y terminó la llamada
durationenteroCuando la llamada tiene sus fechasSegundos, la misma cifra que el billingSeconds de la llamada
from, totextoCasi siempreSaliente: tu línea y luego el cliente. Entrante: el cliente y luego tu línea
callerNumbertextoSiempreEl número del cliente, sea cual sea el sentido de la llamada; "web-call" para una llamada desde un navegador
campaignIdUUID o nullLlamadas de campañaLa campaña que hizo la llamada
endedReasontextoCuando la llamada tiene unoPor qué terminó: consulta Motivos de fin de llamada
isReached, isSignificant, isSilent, isSuccessfulbooleanoUna vez analizada la llamadaEl resultado: consulta Análisis de llamadas
summarytextoUna vez analizada la llamadaEl resumen, vacío cuando nadie habló
transcripttexto o nullCuando se dijo algoUna línea por turno, que empieza por User: o Assistant:
recordingUrlURL o nullAgentes con el motor de voz actualUn enlace directo a la grabación; puede estar vacío
agent{id, name}SiempreEl agente de la llamada
phone{id, number} o nullSiempreTu línea de CallShift
entity{id, externalId, name} o nullSiempreLa entidad de la llamada, null si es compartida: consulta Entidades
qualificationCriteriaobjetoUna vez analizada la llamadaUna clave por criterio del agente, con su valor como texto
contactobjetoCuando el número coincide con un contactoid, 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 to true.
  • Los metadatos están dentro del contacto: contact.metadata.plan, no metadata.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, formatDate con 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 hacerQué debe recibir el flujo
Una conversación completa, hasta el objetivo del agenteisSuccessful en true, un summary claro, todos los criterios esperados
Una llamada que el agente deba transferir, si transfiere llamadasendedReason en assistant-forwarded-call
Cuelga a los dos segundosisSignificant en false, una transcripción casi vacía: el flujo no debe fallar con ella

Luego comprueba que:

  • callId está presente y cambia en cada llamada;
  • callerNumber es el número desde el que llamaste;
  • transcript y summary corresponden a la conversación;
  • están todos los criterios que lee tu flujo, con el nombre que mapeaste;
  • recordingUrl abre la grabación;
  • las fechas aparecen en tu zona horaria en tus herramientas.
SíntomaCausa probableQué hacer
Ninguna ejecución en n8n o MakeURL inaccesible o flujo inactivoRepite las comprobaciones de la URL de más arriba
Sigue sin llegar nadaWebhook no asignado a este agente, o no guardadoRevisa 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 fallaUn mapeo con una ruta incorrectaCompá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:

  1. un nodo Schedule Trigger en n8n, o un escenario programado en Make;
  2. una petición HTTP a GET/v1/calls, filtrada por agente y por periodo (from y to, fechas ISO completas aplicadas a la fecha de creación), con tu clave de API en una cabecera Authorization: Bearer guardada en las credenciales de la herramienta, y leyendo las páginas hasta pageCount (consulta Paginación);
  3. una comprobación de cada callId frente a lo que ya contiene tu destino, para añadir solo las que faltan;
  4. para la transcripción completa de una llamada, GET/v1/calls/{callId}.
Terminal
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 en metadata, 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:

n8n Code node
// 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
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

Prompt para copiar
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

Prompt para copiar
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

Prompt para copiar
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.