Volver al blog
Desarrolladores

Handoff humano con origin en la API de Waspy

Pausá tu bot cuando el último mensaje saliente trae origin panel: lo escribió una persona en el inbox o desde la app del celular. external_api es tu propia integración.

· 12 min
Handoff humano con origin en la API de Waspy

Respuesta corta: cada mensaje de la API trae origin. Si tu sistema responde solo, pausalo cuando el último saliente es panel: lo escribió una persona en el inbox de Waspy, o salió de la app de WhatsApp Business con el número en coexistencia. external_api es tu propia clave. Si pausás por ese valor, el bot se apaga a sí mismo.

La referencia está en handoff humano. El alta del webhook y la firma están en webhooks en Node y Python y en verificar la firma.

Los seis valores

originQuién lo generóQué hace tu bot
inboundEl contactoPodés procesarlo
panelUna persona en el inbox, o la app del celular en coexistenciaPausá
external_apiPOST /messages con tu API keyEs tu envío. No pauses por esto
ai_agentUn flujo de chatbot del productoNo es la señal de una persona
campaignCampaña o aviso programado de un pedidoNo pauses
historyBackfill del historial de MetaIgnoralo
metadata.origin es lo que se guarda. El campo público origin es el que llega en el webhook y en GET /messages/:id. Para mensajes anteriores a abril de 2026, si ese metadata no estaba, Waspy lo deriva: entrante → inbound; source === history → history; con campaña → campaign; enviado por un flujo → ai_agent; con un usuario del panel → panel; el resto → external_api.

Pausar cuando el último saliente es panel

La regla de la documentación de handoff es esta: el bot solo responde un entrante, y deja de responder si el último mensaje saliente tiene origin === 'panel'.

type Origin =
  | 'inbound'
  | 'panel'
  | 'external_api'
  | 'ai_agent'
  | 'campaign'
  | 'history';

type Msg = {
  direction: 'inbound' | 'outbound';
  origin: Origin;
  conversationId: string;
};

const paused = new Set<string>();

export function onWaspyEvent(event: string, data: Msg) {
  if (
    event === 'message.status_changed' &&
    data.direction === 'outbound' &&
    data.origin === 'panel'
  ) {
    paused.add(data.conversationId);
    return;
  }

  if (event !== 'message.received') return;
  if (data.origin !== 'inbound') return;
  if (paused.has(data.conversationId)) return;

  // responder con POST /messages
}

Un message.status_changed con origin: 'external_api' es la confirmación de que tu mensaje pasó a sent, delivered, read o failed. El clientRef de ese evento está en reconciliar estados. No lo uses para pausar.

La app del celular no avisa por webhook

Cuando el número está en coexistencia y alguien contesta desde la app de WhatsApp Business, Waspy guarda ese saliente con origin: 'panel'. Ese insert no emite message.status_changed a tu URL. El próximo message.received sí llega.

Antes de contestar, leé la página reciente:

curl -sS "https://api.waspytech.com/api/v2/conversations/$CONV_ID/messages?limit=50" \
  -H "Authorization: Bearer $WASPY_API_KEY"

El primer GET, sin cursor, trae los mensajes más nuevos (hasta limit, máximo 100), ordenados del más viejo al más nuevo de esa página. El último objeto con direction: "outbound" es el saliente más reciente de la página. El cursor pide mensajes más viejos, no más nuevos.

function lastOutbound(messages: Msg[]): Msg | undefined {
  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].direction === 'outbound') return messages[i];
  }
  return undefined;
}

// si lastOutbound(data)?.origin === 'panel', no respondas

La ruta chequea el scope messages:read. GET /messages/:id también. En scopes el listado figura bajo conversations:read. En una clave con lista de scopes, incluí los dos: si falta el que la ruta exige, la respuesta es 403. Una clave sin scopes tiene acceso completo.

Qué no tratar como handoff

  • external_api. Lo mandaste vos, incluida una reacción.
  • campaign. Una campaña o el aviso de “pedido despachado” que dispara la tienda. El cliente puede escribir igual, y tu bot puede seguir si nadie del equipo tomó el chat.
  • history. Mensajes viejos sincronizados. No reaccionan a lo que acaba de pasar.
  • ai_agent. Lo escribe un flujo de chatbot cuando el producto manda ese mensaje. Sirve para no entrar en loop con ese flujo. La pausa por una persona sigue siendo panel. Waspy no atiende la tienda solo: el inbox lo responde el equipo. Si conectás Claude, Cursor o ChatGPT, el patrón está en MCP y el envío lo confirma alguien, o lo hace tu sistema con esta misma regla.

Para volver a habilitar el bot, definí vos el criterio: por ejemplo, un entrante después de un rato sin otro panel. La API no trae un campo botPaused. El estado vive en tu sistema, por conversationId.

El conversationId viene en message.received y en message.status_changed. El catálogo de esos dos eventos está en webhooks. Listar el chat y asignarlo está en conversaciones por API.

La clave de la API está en los planes Scale y Enterprise. El inbox, donde una persona escribe y el mensaje queda como panel, está desde el plan de entrada. Los dos se ven en Planes y en la API. Se puede probar 10 días sin tarjeta en el registro.

Tu equipo atendiendo por WhatsApp en minutos

CRM facil pero potente. En 10 minutos tenes a tu equipo trabajando — no necesitas hablar con nadie.