developers
EN Crear cuenta

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#

1. Recibe el mensaje#

Cada mensaje de un cliente llega a tu webhook. El data trae todo lo necesario para contestar sin otra llamada:

JSON
{
  "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"
  }
}
CampoPara qué
channel, external_user_idVan tal cual como channel y to de POST /v1/messages.
channel_connection_idEl número o la cuenta por donde entró: pásalo para contestar por el mismo.
bot_paused, bot_paused_bySi es true, una persona (o un flujo) tiene la conversación: tu IA no contesta.
sandboxLa 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
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:

CampoQué hace
reasonPor qué va a una persona, en palabras (hasta 300 caracteres). El equipo lo lee en la bandeja.
urgencyhigh, medium o low. Si la mandas queda fija; si no, se calcula sola.
curl
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" }'
JSON
{
  "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
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
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
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#

TypeScript
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_byQué pasó
api_escalationTu integración pasó la conversación a una persona.
apiTu integración pausó el bot sin escalar.
human_agentAlguien del equipo respondió desde la bandeja o la escaló a mano.
human_deactivated, human_temporaryAlguien del equipo apagó el bot (la temporal vuelve sola: bot_paused_until).
flow:<id>Un flujo de heysteff lleva la conversación.
kanban_columnLa conversación entró a una columna del embudo que pausa el bot.

Errores#

CódigoHTTPQué pasó
invalid_id400El id de la conversación no es un UUID.
validation_error400El cuerpo no pasa la validación (por ejemplo, una urgency desconocida).
forbidden403A la key le falta conversations:write (o conversations:read para leer).
not_found404La conversación no es de este espacio, o no es del sandbox con una key de prueba.
personal_workspace_no_escalation422Espacio personal: pausa el bot en vez de escalar.