Guías
Conecta tu propia IA
Tu modelo contesta; heysteff pone los canales (WhatsApp, Instagram, Messenger, SMS y correo), la bandeja y al equipo. Cuando tu IA no sabe qué responder, le pasa la conversación a una persona y deja de contestar hasta que se la devuelvan.
Antes de empezar#
- Apaga el agente de heysteff (la cuenta de developer lo trae apagado): si no, contestarían los dos.
- Una API key con
messages:write,conversations:readyconversations:write. Con una keysk_test_funciona igual sobre las conversaciones del sandbox. - Un webhook suscrito a
message:received.
1. Recibe el mensaje#
Cada mensaje de un cliente llega a tu webhook. El data trae todo lo necesario para contestar sin otra llamada:
{
"id": "5d1f0c7e-8a2b-4c3d-9e4f-0a1b2c3d4e5f",
"event": "message:received",
"ts": "2026-11-04T14:03:52.104Z",
"data": {
"conversation_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"message_id": "4e3d2c1b-0a9f-4e8d-7c6b-5a4f3e2d1c0b",
"channel": "whatsapp_cloud",
"channel_connection_id": "3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
"external_user_id": "56912345678",
"contact_id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2918",
"bot_paused": false,
"bot_paused_by": null,
"sandbox": false,
"content": "¿Me pueden devolver el dinero del pedido 1042?",
"content_type": "text"
}
}| Campo | Para qué |
|---|---|
channel, external_user_id | Van tal cual como channel y to de POST /v1/messages. |
channel_connection_id | El número o la cuenta por donde entró: pásalo para contestar por el mismo. |
bot_paused, bot_paused_by | Si es true, una persona (o un flujo) tiene la conversación: tu IA no contesta. |
sandbox | La conversación es del sandbox (contéstala con tu key sk_test_). |
message:sent trae los mismos campos. Los de siempre (channel_type, media_url, filename…) no cambian.
2. Contesta#
Si bot_paused es false, pásale el texto a tu modelo y manda la respuesta. Usa el message_id del aviso en la Idempotency-Key: si el webhook llega dos veces, la respuesta sale una.
curl https://app.heysteff.com/api/v1/messages \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reply-4e3d2c1b" \
-d '{
"channel": "whatsapp_cloud",
"to": "56912345678",
"channel_connection_id": "3a2b1c0d-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
"content": "Hola, el pedido 1042 salió ayer. ¿Te ayudo con algo más?"
}'3. Cuando no sepa: pásala a una persona#
POST /v1/conversations/{id}/escalate abre una escalación en la bandeja, pausa el bot (bot_paused_by = api_escalation), la vuelve a abrir si estaba archivada y avisa al equipo con un resumen y su urgencia. Los dos campos son opcionales:
| Campo | Qué hace |
|---|---|
reason | Por qué va a una persona, en palabras (hasta 300 caracteres). El equipo lo lee en la bandeja. |
urgency | high, medium o low. Si la mandas queda fija; si no, se calcula sola. |
curl https://app.heysteff.com/api/v1/conversations/$CONVERSATION_ID/escalate \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Pide un reembolso y no tengo cómo aprobarlo.", "urgency": "high" }'{
"data": {
"escalation_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"already_open": false,
"conversation": {
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"channel": "whatsapp_cloud",
"external_user_id": "56912345678",
"status": "open",
"bot_paused": true,
"bot_paused_by": "api_escalation",
"escalation": { "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e", "reason": "api:handoff", "urgency": "high", "taken": false, "created_at": "2026-11-04T14:03:55.010Z" }
}
}
}Es idempotente: con una escalación abierta responde 200 con "already_open": true y no abre otra, pero el reason nuevo se suma al caso. En un espacio personal no hay equipo: responde 422 personal_workspace_no_escalation y lo que corresponde es pausar el bot.
4. Mientras la tiene el equipo#
Los mensajes del cliente siguen llegando a tu webhook, con bot_paused: true: tu IA no contesta. Si quieres consultar el estado sin esperar un aviso:
curl https://app.heysteff.com/api/v1/conversations/$CONVERSATION_ID \
-H "Authorization: Bearer $HEYSTEFF_API_KEY"Devuelve bot_paused, bot_paused_by, la escalación abierta (con su urgencia y si alguien ya la tomó), la persona asignada y last_inbound_at (en WhatsApp Cloud el texto libre solo sale dentro de las 24 horas siguientes).
5. Devuélvela a tu IA#
Cuando el equipo termina, toca Reactivar bot o Resolver en la bandeja, o lo hace tu sistema:
curl https://app.heysteff.com/api/v1/conversations/$CONVERSATION_ID/bot \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "paused": false }'{"paused": false} reanuda el bot y cierra la escalación abierta. Si un flujo de heysteff sigue llevando la conversación, su pausa se respeta y la respuesta trae "kept_flow_pause": true. Con {"paused": true} pausas sin escalar (bot_paused_by = api), por ejemplo cuando tu sistema decide atender por su cuenta; una pausa que ya existía no se pisa.
curl -X POST https://app.heysteff.com/api/v1/conversations/$CONVERSATION_ID/resolve \
-H "Authorization: Bearer $HEYSTEFF_API_KEY"resolve es el Resolver de la bandeja: cierra las escalaciones, reactiva el bot y archiva la conversación. El próximo mensaje del cliente la vuelve a abrir.
Todo junto con el SDK#
import express from 'express';
import { Heysteff, verifyWebhook, type MessageWebhookData } from '@heysteff/sdk';
const heysteff = new Heysteff({ apiKey: process.env.HEYSTEFF_API_KEY });
const app = express();
app.post('/webhooks/heysteff', express.raw({ type: 'application/json' }), async (req, res) => {
const event = await verifyWebhook<MessageWebhookData>({
payload: req.body,
headers: req.headers,
secret: process.env.HEYSTEFF_WEBHOOK_SECRET!,
});
res.sendStatus(200); // responde rápido y procesa después
if (event.event !== 'message:received') return;
const msg = event.data;
// Una persona del equipo tiene la conversación: tu IA no contesta.
if (msg.bot_paused) return;
const answer = await miModelo(msg.content ?? ''); // { text, confident }
if (!answer.confident) {
await heysteff.conversations.escalate(msg.conversation_id, {
reason: 'No sé si el pedido se puede reembolsar.',
});
return;
}
await heysteff.messages.send(
{
channel: msg.channel,
to: msg.external_user_id,
...(msg.channel_connection_id ? { channel_connection_id: msg.channel_connection_id } : {}),
content: answer.text,
},
{ idempotencyKey: `reply-${msg.message_id}` },
);
});Con el SDK también están conversations.get, conversations.pauseBot, conversations.resumeBot y conversations.resolve. Desde la terminal: heysteff conversations get|escalate|pause|resume|resolve <id> (CLI).
Quién pausó el bot#
| bot_paused_by | Qué pasó |
|---|---|
api_escalation | Tu integración pasó la conversación a una persona. |
api | Tu integración pausó el bot sin escalar. |
human_agent | Alguien del equipo respondió desde la bandeja o la escaló a mano. |
human_deactivated, human_temporary | Alguien del equipo apagó el bot (la temporal vuelve sola: bot_paused_until). |
flow:<id> | Un flujo de heysteff lleva la conversación. |
kanban_column | La conversación entró a una columna del embudo que pausa el bot. |
Errores#
| Código | HTTP | Qué pasó |
|---|---|---|
invalid_id | 400 | El id de la conversación no es un UUID. |
validation_error | 400 | El cuerpo no pasa la validación (por ejemplo, una urgency desconocida). |
forbidden | 403 | A la key le falta conversations:write (o conversations:read para leer). |
not_found | 404 | La conversación no es de este espacio, o no es del sandbox con una key de prueba. |
personal_workspace_no_escalation | 422 | Espacio personal: pausa el bot en vez de escalar. |