Callsistdocs
Guías

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

El 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@servidor

y 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:

ScopeLo usa
files:writeSubir el audio
transcripts:writeEncolar la transcripción
transcripts:readSondear y leer el resultado
llm:invokeEl análisis con plantilla
embeddings:invokeEmbeddings, si montas RAG
usage:readConsultar 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):

CampoValor
NameAuthorization
ValueBearer sk_live_…

En cada nodo HTTP Request: Authentication → Generic Credential Type → Header Auth → la credencial.


5. Comprobación de humo

Manual TriggerHTTP 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 BodyBody Content Type: Form-Data
  • Body Parameters, un parámetro:
    • Parameter Type: n8n Binary File
    • Name: file ← el nombre del campo no es negociable; otro da 400 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 HeadersIdempotency-Key = ={{ $json.id }}
  • Send BodyJSON:
{
  "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-Key es el file_id, no un aleatorio. Así, re-ejecutar desde este nodo con la misma subida devuelve la transcripción original con Idempotency-Replayed: true y no la vuelve a cobrar. Con un uuid nuevo por ejecución, cada clic en «Test workflow» es un cargo.
  • language_hints es lo único del bloque de idioma que se usa. language_confidence_threshold, speaker_labels, speakers_expected, channels y word_timestamps se 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.scope solo admite "customer". "agent" es 400 sentiment_scope_not_permitted: lo prohíbe el Reglamento de IA (art. 5.1.f), no es un bug pendiente.
  • retention explí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 }}:

RamaCondiciónA dónde va
ListacompletedAl análisis (§7)
FallofailedA tu manejo de errores. Un fallo no se factura nunca
SiguerestoDe 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_completion como 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
Information Extractor / Text Classifier / Summarization Chain
Embeddings OpenAISí (ver abajo)
AI Agent, Tools Agent, cualquier cosa con herramientasNo

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ímiteValorQué hacer en n8n
Transcripciones en vuelo32 por organizaciónNodo Loop Over Items, Batch Size 8
Peticiones por minuto300 por claveDos claves: una transcribe, otra analiza
Tamaño de fichero2 GB
Duración de audio10 hTrocear 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)

On this page