n8n
El recorrido completo con nodos de n8n, y el nodo que no sirve.
Cómo montar el recorrido completo —subir audio, transcribir, analizar— con nodos de n8n.
Procedencia. Lo que dice esta guía sobre la API está verificado contra el código y contra producción, como el resto de esta documentación. Lo que dice sobre n8n está escrito contra n8n 1.x a partir de la documentación de sus nodos y no se ha ejecutado: los rótulos de la interfaz cambian entre versiones. Si uno no coincide, el que manda es el comportamiento de la API, que sí está comprobado.
1. Tres cosas que condicionan el diseño del workflow
No hay nodo de Callsist en n8n. Todo va con HTTP Request genérico. Para chat y embeddings se pueden reutilizar los nodos de OpenAI cambiándoles la Base URL, con una excepción grande que está en §7.
No hay modo de pruebas. Las claves sk_test_ se retiraron: cada ejecución consume
saldo real. No es caro —el recorrido entero sobre un audio corto son ~0,011 €, §10— pero
sí es real, y un bucle mal cerrado gasta de verdad.
La transcripción es asíncrona. POST /v1/transcripts devuelve 202 y un id; el
resultado se recoge sondeando o por webhook. En n8n eso es un ciclo Wait → HTTP Request → IF con el cable de vuelta, y es la parte donde se concentran los errores.
2. Levantar n8n
docker run -d --name n8n \
-p 127.0.0.1:5678:5678 \
-v n8n_data:/home/node/.n8n \
-e GENERIC_TIMEZONE=Europe/Madrid \
docker.n8n.io/n8nio/n8nEl 127.0.0.1: delante del puerto no es decoración: sin él queda expuesto un panel que
va a tener dentro una sk_live_. Para llegar desde fuera, túnel SSH:
ssh -L 5678:127.0.0.1:5678 usuario@servidory abrir http://localhost:5678. Si prefieres n8n en tu máquina, npx n8n hace lo mismo.
Da igual dónde corra mientras tenga salida a internet — salvo que quieras webhooks (§8),
que sí exigen que Callsist llegue a n8n por HTTPS público.
3. La clave y sus scopes
Se crea en https://app.callsist.com/keys y se muestra una sola vez. Para el
recorrido completo hacen falta seis scopes:
| Scope | Lo usa |
|---|---|
files:write | Subir el audio |
transcripts:write | Encolar la transcripción |
transcripts:read | Sondear y leer el resultado |
llm:invoke | El análisis con plantilla |
embeddings:invoke | Embeddings, si montas RAG |
usage:read | Consultar el saldo antes del lote |
Si la clave lleva lista blanca de IPs, la que tiene que estar es la IP de salida del
servidor donde corre n8n, no la del navegador con el que editas el workflow. Y el
diagnóstico despista: una IP fuera de la lista no devuelve ip_not_allowed —ese código
está publicado y no se emite nunca— sino 403 missing_scope con param: "ip_allowlist".
4. La credencial
Credentials → New → Header Auth (la genérica):
| Campo | Valor |
|---|---|
| Name | Authorization |
| Value | Bearer sk_live_… |
En cada nodo HTTP Request: Authentication → Generic Credential Type → Header Auth → la credencial.
5. Comprobación de humo
Manual Trigger → HTTP Request a https://api.callsist.com/v1/me.
Si devuelve la organización y la lista de scopes, la autenticación está resuelta y el
resto de la guía aplica. Un segundo nodo a /v1/balance te dice si hay saldo. Ninguno de
los dos cobra.
6. El flujo de transcripción, nodo a nodo
6.1 · Read/Write Files from Disk
Operación Read File(s), apuntando a un .mp3 o .wav. El binario sale en el campo
data. Si prefieres no meter ficheros en el contenedor, salta este nodo y usa
audio_url con una URL pública en §6.3. Con fichero subido la estimación de coste es
exacta porque se conoce el tamaño; con URL se asume una duración conservadora.
6.2 · HTTP Request — POST /v1/files
- URL
https://api.callsist.com/v1/files - Send Body → Body Content Type: Form-Data
- Body Parameters, un parámetro:
- Parameter Type: n8n Binary File
- Name:
file← el nombre del campo no es negociable; otro da400 missing_parameter - Input Data Field Name:
data
Sin cabecera Idempotency-Key en este nodo. El middleware hashea el cuerpo entero
para detectar reintentos, y con un multipart eso es cargar hasta 2 GB en memoria
(ver Subir audio). Una subida duplicada solo deja un fichero huérfano: no
cobra.
Devuelve { "id": "file_…" }.
6.3 · HTTP Request — POST /v1/transcripts
- Send Headers →
Idempotency-Key=={{ $json.id }} - Send Body → JSON:
{
"file_id": "={{ $json.id }}",
"language": "auto",
"language_hints": ["es", "ca"],
"understanding": {
"topic_detection": { "taxonomy": "contact_center" },
"content_moderation": {},
"pii_redaction": { "policy": "entity_name" },
"profanity_filter": { "policy": "mask" },
"sentiment_analysis": { "scope": "customer" },
"summarization": { "format": "bullets" },
"action_items": true
},
"retention": "30d",
"metadata": { "origen": "n8n", "ticket": "T-91823" }
}Cinco decisiones metidas ahí:
- La
Idempotency-Keyes elfile_id, no un aleatorio. Así, re-ejecutar desde este nodo con la misma subida devuelve la transcripción original conIdempotency-Replayed: truey no la vuelve a cobrar. Con un uuid nuevo por ejecución, cada clic en «Test workflow» es un cargo. language_hintses lo único del bloque de idioma que se usa.language_confidence_threshold,speaker_labels,speakers_expected,channelsyword_timestampsse aceptan, no dan error y no cambian nada (los cinco parámetros que se ignoran). No construyas ramas del workflow encima de ellos.- Los siete módulos o ninguno. El pack (0,29 €/h) solo se aplica con los siete; seis sueltos son 0,35 €/h. Pedir seis sale más caro que pedir siete.
sentiment_analysis.scopesolo admite"customer"."agent"es400 sentiment_scope_not_permitted: lo prohíbe el Reglamento de IA (art. 5.1.f), no es un bug pendiente.retentionexplícito aunque coincida con el defecto de la organización: deja la intención escrita en la petición y no en una configuración que alguien puede cambiar.
Nada de webhook_url en el cuerpo: se rechaza con 400. Los webhooks se registran
aparte (§8).
Y metadata es lo que hace útil la integración desde n8n en particular: ahí va el id del
ticket que traiga el trigger, y vuelve tal cual al leer la transcripción. Sin eso hay que
mantener una tabla de correspondencias fuera del workflow.
6.4 · Wait
Resume: After Time Interval, 15 segundos.
6.5 · HTTP Request — sondear
=https://api.callsist.com/v1/transcripts/{{ $('POST transcripts').item.json.id }}Referencia el nodo del 202 por su nombre, no $json: en el ciclo, $json viene del
Wait o de la vuelta anterior.
6.6 · Switch — la salida del ciclo
Tres ramas sobre {{ $json.status }}:
| Rama | Condición | A dónde va |
|---|---|---|
| Lista | completed | Al análisis (§7) |
| Fallo | failed | A tu manejo de errores. Un fallo no se factura nunca |
| Sigue | resto | De vuelta al nodo Wait |
Ese cable hacia atrás es el bucle. Dos guardas que no son opcionales:
- Un tope de vueltas. n8n no limita las iteraciones de un ciclo. Añade una condición
con
{{ $runIndex }}—cuántas veces ha corrido el nodo en esta ejecución— y corta a las ~40, que con 15 s son diez minutos. - No uses
estimated_completioncomo timeout. Es siempre «ahora + 6 minutos», una constante que no mira ni la cola ni la duración del audio (ver Límites).
El estado cancelled se puede filtrar en el listado pero no lo produce nadie: no hay
endpoint de cancelación. No hace falta rama para él.
7. Leer el resultado sin creerse lo que no llegó
Un nodo Code entre el completed y lo que venga después:
const t = $input.first().json;
const w = t.warnings ?? [];
// 1. ¿Son fiables los roles agente/cliente?
const rolesFiables =
!w.some(x => ['attribution_heuristic_disagrees',
'diarization_single_speaker',
'diarization_unavailable'].includes(x)) &&
(t.speaker_role_confidence ?? 0) >= 0.6;
// 2. Con redacción activa, el texto tapado NO es `text`.
const texto = t.pii ? t.pii.text_redacted : t.text;
const turnos = t.pii ? t.pii.utterances_redacted : t.utterances;
// 3. `understanding` es {} cuando no se pidió nada: comprueba claves, no el objeto.
const resumen = t.understanding?.summarization?.text ?? null;
return [{ json: { rolesFiables, texto, turnos, resumen, warnings: w } }];Las tres comprobaciones, por orden de lo que cuesta descubrirlas tarde:
text y utterances siguen en claro aunque actives la redacción. Es deliberado
—quien no la pide quiere su transcripción entera— pero significa que un workflow que
active pii_redaction y luego lea text está procesando datos personales en claro
creyendo que no. En n8n se pisa con especial facilidad, porque el panel de salida del
nodo enseña text arriba del todo y pii hay que ir a buscarlo.
warnings es la lista de lo que pediste y no llegó. Un módulo que aparece ahí no
está en understanding, y tratarlo como «vacío» es un error de datos disfrazado de
resultado. Un módulo con warning tampoco se factura.
Los roles necesitan las dos señales, no solo el número. La confianza medida en
llamadas reales es 0,9 en castellano. Pero en una llamada en catalán salió 0,7 —por
encima de cualquier umbral razonable— y los roles estaban invertidos: el saludo del
agente venía como customer. Lo dijo attribution_heuristic_disagrees, en warnings.
Si vas a puntuar agentes desde n8n, las llamadas que no pasen esta comprobación hay que
apartarlas para revisión, no puntuarlas: una plantilla sobre roles invertidos produce
una nota confiada y falsa, que es peor que no tener nota.
8. Los nodos de OpenAI: dónde sirven y dónde no
/v1/chat/completions y /v1/embeddings tienen la forma de la API de OpenAI, así que
los nodos de LangChain de n8n funcionan. Crea una credencial OpenAI con
API Key = tu sk_live_… y Base URL = https://api.callsist.com/v1, y escribe el
ID del modelo a mano (callsist-llm-1-small, callsist-llm-1-large,
callsist-embed-1).
⛔ No lo enchufes a un nodo AI Agent
Los agentes de n8n funcionan a base de tool calling y Callsist no tiene: tools y
tool_choice se descartan en silencio, sin error ni warning. El resultado no es un
fallo limpio — el modelo contesta en prosa, n8n no encuentra la llamada a herramienta que
espera y el nodo revienta con un mensaje que no apunta a la causa.
| Nodo | ¿Sirve? |
|---|---|
| Basic LLM Chain | Sí |
| Information Extractor / Text Classifier / Summarization Chain | Sí |
| Embeddings OpenAI | Sí (ver abajo) |
| AI Agent, Tools Agent, cualquier cosa con herramientas | No |
Dos límites más del mismo tipo: content solo admite cadena —el formato multimodal de
OpenAI se rechaza con 400— y top_p, seed, stop, n y las penalizaciones se
descartan sin avisar. n8n manda algunos por defecto: no rompen nada y tampoco hacen nada.
Para extracción, HTTP Request directo
Más control que el nodo: temperature: 0, response_format: { "type": "json_object" },
el esquema pedido campo por campo en el prompt. El JSON no está garantizado a nivel de
gramática, así que valida la salida y reintenta una vez. Y mira finish_reason: con
"length" el JSON viene truncado y parsearlo falla de una forma que parece otra cosa.
reasoning_effort no hace falta mandarlo: Callsist pone "none", que es lo correcto para
clasificar y extraer. Si lo pones a "low" no ahorras nada —gasta lo mismo que el
defecto—; "medium" y "high" multiplican los tokens de salida.
Pon timeout en el nodo (Settings → Timeout, 120 s). El cliente HTTP interno de Callsist tiene 120 s y hasta 3 reintentos, así que en el peor caso una petición de chat tarda varios minutos en devolver un error, y no hay forma de acotarlo desde la petición (ver Límites). Sin timeout propio, el workflow se queda colgado ahí.
Embeddings
Deja Dimensions vacío. Callsist aplica 768 por defecto, y si pides una dimensión que el proveedor no devuelve exactamente, la petición falla en vez de guardarte vectores del tamaño equivocado. La dimensión de tu almacén de vectores tiene que ser 768.
Callsist no almacena ni busca vectores: devuelve los vectores y ahí acaba. El almacén (pgvector, Qdrant, el que sea) lo pones tú, y el RAG en n8n es el patrón normal —embeber, recuperar, meter los fragmentos en el prompt— sin tool calling por medio.
9. Webhooks en vez de sondeo
Solo si n8n está expuesto en HTTPS público. Registro con POST /v1/webhooks, la URL de
producción del nodo Webhook (no la de test, que solo vive mientras miras la pantalla)
y events: ["transcript.completed", "transcript.failed"]. El whsec_… se devuelve una
sola vez.
Tres cosas que se atascan siempre:
La firma va sobre el cuerpo crudo. Hay que activar Options → Raw Body en el nodo
Webhook; sin eso n8n entrega el JSON ya parseado, y el HMAC calculado sobre un objeto
re-serializado no cuadra nunca. Se firma {timestamp}.{cuerpo} con HMAC-SHA256 y la
tolerancia contra reenvío es de 300 s.
El payload no trae la transcripción —pesaría megabytes y se reenviaría en cada
reintento—, solo transcript_id, status, language y audio_duration. Al recibirlo,
GET /v1/transcripts/{transcript_id}.
Responde 2xx rápido y procesa después. Nodo Respond to Webhook al principio de la
rama, no al final. Hay cinco reintentos —1 min, 5 min, 30 min, 2 h, 6 h— y a los cinco
fallos el endpoint se desactiva solo; el aviso de que se ha desactivado va por correo,
porque el canal que habría que usar es justo el que acaba de fallar.
Para un lote propio, sondear es más simple y más robusto: no necesitas endpoint público, ni TLS, ni verificar firmas, y una caída de n8n no pierde el aviso — al volver, sondea y ya está.
10. Del ejemplo al lote
| Límite | Valor | Qué hacer en n8n |
|---|---|---|
| Transcripciones en vuelo | 32 por organización | Nodo Loop Over Items, Batch Size 8 |
| Peticiones por minuto | 300 por clave | Dos claves: una transcribe, otra analiza |
| Tamaño de fichero | 2 GB | — |
| Duración de audio | 10 h | Trocear antes |
Los dos que muerden de verdad:
El sondeo come del mismo cubo que el trabajo. No hay límite aparte para consultar
estado. Sondear 400 transcripciones cada 10 s son 2400 peticiones por minuto: 429
inmediato. Sondea solo lo que está en vuelo. Y como el cubo es por clave y no por
organización, separar «la clave que transcribe» de «la clave que analiza» es la forma
barata de que un lote no ahogue al otro.
El Retry On Fail del nodo no lee Retry-After. Espera lo que le pongas en Wait
Between Tries. Para 429, 500 y 503 sirve igual poniendo 30 s; para el resto no
actives el reintento: 400, 401, 403, 409, 413, 415 y 422 no cambian por
insistir, y solo gastan cuota. El 402 es aparte — para el lote y recarga; seguir
mandando peticiones solo llena el log de errores.
Guarda request_id en tus datos: viene en la cabecera Callsist-Request-Id de toda
respuesta y es lo que se busca en https://app.callsist.com/logs.
Un último aviso operativo que no es de n8n pero afecta a lo que montes encima:
expires_at se escribe y no hay nada que borre lo caducado
(ver Retención y borrado). Si necesitas que algo desaparezca de verdad, el workflow
tiene que llamar a DELETE /v1/transcripts/{id}.
11. Qué cuesta la prueba
Un audio de 30 s con los siete módulos —se factura el minuto empezado:
ASR 1 min × 0,39 €/h = 0,0065 €
Und. pack 1 min × 0,29 €/h = 0,0048 €
─────────
0,011 €Más el análisis, si lo añades: con callsist-llm-1-small y una transcripción corta, unos
0,01 € por ejecución. Poner GET /v1/balance como primer nodo del workflow te lo enseña
antes y después.
12. El workflow, de un vistazo
Manual Trigger
└─ Read/Write Files from Disk (binario en `data`)
└─ HTTP Request POST /v1/files → file_id · sin Idempotency-Key
└─ HTTP Request POST /v1/transcripts → tr_… (202) · Idempotency-Key = file_id
└─ Wait 15 s ◀────────────────────┐
└─ HTTP Request GET /v1/transcripts/{id}
└─ Switch status
├─ completed → Code (warnings, pii, roles) → análisis
├─ failed → manejo de errores
└─ resto ─────────┘ (tope con $runIndex)