Handoff a humano

Si tienes un bot externo que conversa con tus clientes vía la API, puedes escalarle una conversación a un asesor humano cuando el cliente lo pida. Un solo POST marca la conversación como "pendiente de asesor" y dispara toda la maquinaria que ya vive en el inbox: badge, push a los asesores, recordatorios y el webhook saliente.

El endpoint

POST /organizations/{orgId}/conversations/{conversationId}/request-handoff
  • Auth: X-Api-Key con scope conversations:write (o una key sin scopes = acceso total).
  • Body opcional: { "detail": "texto libre" } — queda en el log y se incluye en el webhook saliente (ej. "cliente pidió hablar con ventas").
  • Idempotente: si la conversación ya estaba en handoff, no se vuelve a disparar (no duplica push ni mensajes).
  • Se respeta a cualquier hora: el traspaso diferido de Fuera de horario aplica solo a los pedidos que nacen del bot.

Ejemplo

# Bash / curl
curl -X POST \
  "https://api.mosend.dev/organizations/${ORG_ID}/conversations/${CONVERSATION_ID}/request-handoff" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{ "detail": "el cliente pidió hablar con un asesor de ventas" }'

# Respuesta (200)
# { "data": { "ok": true }, "timestamp": "..." }
// Node.js (18+)
await fetch(
  `https://api.mosend.dev/organizations/${ORG_ID}/conversations/${conversationId}/request-handoff`,
  {
    method: 'POST',
    headers: { 'X-Api-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ detail: 'el cliente pidió un asesor' }),
  },
);
# Python
import requests
requests.post(
    f"https://api.mosend.dev/organizations/{ORG_ID}/conversations/{conversation_id}/request-handoff",
    headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"},
    json={"detail": "el cliente pidió un asesor"},
    timeout=15,
)

Qué pasa al dispararlo

Una sola llamada activa, en cadena:

  • La conversación queda marcada como pendiente de asesor: aparece la etiqueta/badge en el inbox y entra al filtro "Pendientes de asesor" en tiempo real.
  • Se silencia cualquier automatización de Mosend sobre esa conversación (bot interno, auto-respuestas), para que no pise al asesor.
  • La conversación queda sin asignar para que cualquier asesor disponible la pueda tomar.
  • Se manda push destacado a los asesores, con recordatorios a los 2, 5 y 10 minutos si nadie la reclama.
  • Si la org configuró un mensaje de handoff, se le envía al cliente automáticamente.
  • Se dispara el webhook saliente conversation.handoff_requested (si tienes uno suscrito) — útil si quieres además notificar a Slack, Teams, etc.

Flujo recomendado para un bot externo

  1. Tu bot conversa con el cliente vía POST /messages.
  2. Cuando detectas (con tu propia lógica/IA) que el cliente quiere un humano — por ejemplo escribe "quiero hablar con una persona" — llamas a request-handoff sobre esa conversación.
  3. Tu bot deja de responder en esa conversación (tú controlas eso de tu lado).
  4. Los asesores ven la solicitud en el inbox de Mosend y la atienden.

Fuera de horario

Si el cliente pide asesor cuando el equipo está cerrado, traspasar no sirve: nadie va a tomar la conversación y el bot quedaría mudo hasta la mañana. Por eso, cuando el pedido nace del bot (herramienta del agente IA, palabra clave, regla o flujo) y el horario está cerrado, no hay handoff: el bot crea una tarea para el equipo, le dice al cliente cuándo lo contactan y sigue atendiendo.

  • El horario que manda es el del número (BotConfig.outOfHoursSchedule) si lo tiene; si no, el de la organización (Organization.businessHoursSchedule). Sin ninguno se asume 24/7 y esto no aplica.
  • Los traspasos manuales (desde el inbox) y por API (request-handoff) se respetan siempre, abierto o cerrado.
  • La tarea va a la cola del equipo (origin: BOT, sin dueño) y vence a la próxima apertura más los minutos de gracia; si el cliente insiste esa noche, se anota en la misma tarea y no se le repite el aviso. Se completa sola cuando una persona responde.
  • Queda un evento HANDOFF_DEFERRED en bot-events con reason, taskId, apertura y si se avisó al cliente. El webhook conversation.handoff_requested no se dispara.

Se configura por número en PUT /organizations/{orgId}/bot/config/{phoneId}:

CampoDefaultQué hace
outOfHoursTaskEnabledtrueActiva el traspaso diferido. En false el handoff fuera de horario ocurre como siempre (bot mudo hasta que alguien lo tome).
outOfHoursTaskMessagenullTexto al cliente. Admite {apertura} ("mañana a las 8:00 a. m."). null = texto por defecto; cadena vacía = no avisar nada.
outOfHoursTaskGraceMin30Minutos después de abrir en los que vence la tarea (ahí sale el recordatorio al equipo).
curl -X PUT "https://api.mosend.dev/organizations/${ORG_ID}/bot/config/${PHONE_NUMBER_ID}" \
  -H "X-Api-Key: ${API_KEY}" -H "Content-Type: application/json" \
  -d '{
    "outOfHoursTaskEnabled": true,
    "outOfHoursTaskMessage": "Ya dejé tu solicitud al equipo; te escriben {apertura}. Mientras tanto sigo aquí para lo que necesites.",
    "outOfHoursTaskGraceMin": 30
  }'

Seguimiento por silencio

Cuando el bot responde y el cliente se queda callado, el bot puede retomar una sola vez y, si sigue el silencio, cerrar la conversación con una despedida. Apagado por defecto; se configura en el mismo PUT …/bot/config/{phoneId}:

CampoDefaultQué hace
followUpEnabledfalseActiva el seguimiento.
followUpAfterMin15Minutos de silencio tras la última respuesta del bot antes de retomar.
followUpCloseAfterMin60Minutos más de silencio, después del seguimiento, antes de cerrar la conversación.
followUpMode"AI"AI: la IA redacta la frase con el contexto y puede callar si no quedó nada pendiente. TEXT: envía followUpMessage tal cual.
followUpMessagenullTexto del seguimiento en modo TEXT.
followUpCloseMessagenullDespedida al cerrar. null = despedida por defecto; cadena vacía = cerrar sin enviar nada.
  • Nunca corre si la conversación tiene asesor asignado, un handoff pendiente, un flujo esperando respuesta, un contacto dado de baja (opt-out) o la ventana de 24 horas cerrada.
  • Eventos en bot-events: FOLLOWUP_SENT (se retomó), FOLLOWUP_SKIPPED (la IA decidió que no había nada pendiente, motivo: nada-pendiente) y FOLLOWUP_CLOSED (se cerró por silencio).
curl -X PUT "https://api.mosend.dev/organizations/${ORG_ID}/bot/config/${PHONE_NUMBER_ID}" \
  -H "X-Api-Key: ${API_KEY}" -H "Content-Type: application/json" \
  -d '{
    "followUpEnabled": true,
    "followUpAfterMin": 15,
    "followUpCloseAfterMin": 60,
    "followUpMode": "TEXT",
    "followUpMessage": "¿Sigues ahí? Si quieres, retomamos donde quedamos.",
    "followUpCloseMessage": ""
  }'

Notas

  • Necesitas el conversationId de Mosend. Lo obtienes de GET /conversations, del webhook message.new (campo data.conversationId), o de la respuesta de POST /messages.
  • Si tu API key tiene scope por phone-number, solo puedes escalar conversaciones de los números permitidos.
  • Para recibir la notificación de handoff en tu sistema (en vez de dispararla), suscríbete al evento conversation.handoff_requested en Webhooks salientes.