Webhooks
Configura una URL de webhook y Sendveo le retransmite cada mensaje entrante real en tiempo real, para que tus sistemas reaccionen sin sondeo.
Configura tu webhook
Configura o reemplaza tu endpoint. La respuesta incluye un secret compartido, que se muestra cuando el webhook se crea o cuando lo rotas. La URL debe ser accesible públicamente sobre HTTPS.
curl -X PUT https://api.sendveo.com/v1/webhook \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.yourapp.com/sendveo/inbound" }'{
"webhook": {
"url": "https://hooks.yourapp.com/sendveo/inbound",
"active": true,
"secret": "whsec_2f8b41c9a7e04d6fb0c3e1a95d72"
}
}El payload de retransmisión
Cada mensaje entrante se entrega como un POST JSON a tu URL:
{
"type": "message.received",
"message": {
"id": "msg_7Qk2",
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"from": "+212670111222",
"senderName": "Khadija Alaoui",
"text": "Hi, is the order still available?",
"timestamp": "2026-09-09T14:02:11Z"
}
}Responde a un mensaje enviando a su chatId: POST /v1/messages con { channelId, to: chatId, text }.
Catálogo de eventos
Se envían dieciséis tipos de evento por POST a tu URL, todos con el secreto compartido en la cabecera x-sendveo-secret. Cada evento lleva occurredAt (cuándo ocurrió, no cuándo lo retransmitimos). Los ocho eventos de mensaje llevan el mensaje entero bajo message, con la misma forma que usa message.received, así que un solo parser lee los ocho; los tres eventos de grupo tratan de una conversación y llevan chat en su lugar; los cinco eventos de número tratan de una línea y llevan number.
| Evento | Se dispara cuando | Campos adicionales |
|---|---|---|
| message.received | Un contacto envía un mensaje. | ninguno |
| message.sent | Tu número envió un mensaje, desde el propio teléfono o a través de Sendveo. | message.origin |
| message.delivered | Un mensaje saliente llegó al dispositivo del destinatario. | ninguno |
| message.read | Se leyó un mensaje saliente. | ninguno |
| message.failed | Se rechazó un mensaje saliente. | reason |
| message.reaction | Alguien reaccionó o retiró su reacción. | emoji, by |
| message.edited | El remitente editó el mensaje. | by |
| message.deleted | El mensaje se eliminó para todos. | by |
| group.member_joined | Alguien fue añadido a un grupo. | chat, member, by |
| group.member_left | Alguien salió de un grupo, o fue eliminado. | chat, member, by |
| group.updated | Se creó un grupo, o cambió su asunto. | chat, subject, by |
| number.disconnected | el enlace con el teléfono dejó de funcionar | number, reason |
| number.reconnected | el enlace vuelve a funcionar | number |
| number.phone_offline | el enlace está intacto pero el teléfono no responde | number |
| number.phone_online | el teléfono vuelve a responder | number |
| number.history_synced | se ha importado el historial reciente de un número recién vinculado | number, counts |
{
"type": "message.read",
"occurredAt": "2026-09-16T11:01:00Z",
"by": "+212670111222",
"message": {
"id": "msg_8Rm5",
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"from": null,
"senderName": null,
"text": "Votre commande est prete.",
"timestamp": "2026-09-16T10:59:00Z",
"type": "text",
"status": "READ",
"attachments": [],
"replyTo": null,
"location": null,
"contacts": null,
"linkPreview": null
}
}{
"type": "message.reaction",
"occurredAt": "2026-09-16T11:03:00Z",
"emoji": "❤️",
"by": "+212670111222",
"message": { "id": "msg_8Rm5", "chatId": "chat_aa11", "type": "text", "status": "READ" }
}La retirada de una reacción es el mismo evento message.reaction con "emoji": null, no un octavo tipo: un solo manejador cubre las dos mitades.
Las reentregas no se retransmiten dos veces: el servicio de mensajería reentrega los eventos igual que reentrega los mensajes, y una repetición se deduplica, así que tu webhook se dispara una sola vez. Un evento sobre un mensaje que Sendveo no conserva (una conversación anterior a la conexión) se acusa y se descarta: no se retransmite y no es un error.
Eventos de grupo
Los tres eventos de grupo llevan chat donde los eventos de mensaje llevan message, más by (quién lo hizo). chat.members es la lista reducida de participantes: quién, su rol y si es tu propio número.
{
"type": "group.member_joined",
"occurredAt": "2026-09-16T11:40:00Z",
"by": "+212661234567",
"member": { "phone": "+212612345678", "name": "Hind Zerouali", "removed": false },
"chat": {
"id": "cht_m8k3x7q2a1b4",
"chatId": "120363001122334455@g.us",
"channelId": "chn_sales01",
"kind": "group",
"name": "Atlas team - deliveries",
"members": [
{ "phone": "+212661234567", "name": null, "role": "super_admin", "self": true },
{ "phone": "+212612345678", "name": "Hind Zerouali", "role": "member", "self": false }
]
}
}{
"type": "group.updated",
"occurredAt": "2026-09-16T11:45:00Z",
"by": "+212661234567",
"subject": "Atlas team - deliveries 2026",
"chat": { "id": "cht_m8k3x7q2a1b4", "chatId": "120363001122334455@g.us", "kind": "group" }
}Que te eliminen y salir por tu cuenta son UN solo evento, group.member_left, y member.removed los distingue: la misma decisión de forma que toma message.reaction para una reacción y su retirada. Crear un grupo y renombrarlo también son un solo evento, group.updated, porque para un manejador ambos dicen "los detalles de este grupo han cambiado"; subject es el asunto después del cambio.
Un mensaje enviado en un grupo llega como un message.received corriente que lleva message.group (qué conversación) y message.author (quién lo dijo): no hay un evento aparte para los mensajes de grupo. Ambos campos son null en un mensaje uno a uno, que es exactamente lo que sigue viendo un manejador escrito antes de que existieran los grupos.
Un evento sobre una conversación en un número que Sendveo no conoce se acusa y se descarta, como cualquier otro evento de cuenta desconocida. No se retransmite y no es un error.
Eventos de número
Cinco eventos tratan de un número y no de un mensaje o una conversación, así que llevan number donde los demás llevan message o chat. number tiene la misma forma que devuelve GET /v1/numbers/:id, estado y salud juntos, de modo que un solo parser lee ambos.
{
"type": "number.disconnected",
"occurredAt": "2026-09-16T10:05:00Z",
"reason": "SESSION_EXPIRED",
"number": {
"id": "chn_sales01",
"displayName": "Sales line",
"declaredNumber": "+212661234567",
"pairedNumber": "+212661234567",
"status": "CREDENTIALS",
"health": {
"phoneOnline": false,
"lastSeenAt": "2026-09-16T09:58:00Z",
"lastEventAt": "2026-09-16T10:05:00Z"
}
}
}reason solo viaja en number.disconnected: una forma por nombre de evento, la regla que ya siguen los eventos de mensaje y de grupo. Trata number.disconnected como algo por lo que interrumpir a una persona y number.phone_offline como algo que se deja pasar: mezclar los dos hace que un cliente rehaga toda la vinculación porque su teléfono estaba cargando en la habitación de al lado.
Un navegador con una sesión del panel abierta puede leer los mismos dieciséis eventos en vivo en GET /app/events. No hay equivalente con clave de API, y es deliberado: un EventSource de navegador no puede enviar ninguna cabecera Authorization, así que un stream en la API significaría una clave de API en una cadena de consulta, en el historial del navegador y en cada log de proxy. Para un servidor, el webhook es el canal de push.
Versionado del payload
Los campos se añaden, nunca se renombran. message.received conserva exactamente los siete campos que tenía en V1 (id, channelId, chatId, from, senderName, text, timestamp), con los mismos nombres y significados; type, status, attachments, replyTo, location, contacts y linkPreview se suman a ellos. Un manejador escrito contra V1 sigue funcionando, y por eso el pie de una foto sigue llegando en text.
type aparece dos veces y significa dos cosas: en lo alto del sobre es el evento (message.read); dentro de message es el tipo del mensaje (image). El de dentro se nombra igual que en el recurso mensaje, así que un solo analizador lee los dos.
Verificar una retransmisión
Cada retransmisión lleva tu secreto compartido en la cabecera x-sendveo-secret. Compáralo con el valor de la configuración de tu webhook y rechaza cualquier cosa que no coincida.
app.post("/sendveo/inbound", (req, res) => {
const secret = req.header("x-sendveo-secret")
if (secret !== process.env.SENDVEO_WEBHOOK_SECRET) {
return res.status(401).end()
}
const { message } = req.body
handleInbound(message)
res.status(200).end()
})Qué se retransmite
Se retransmite cada evento de las tablas de arriba. Tus propios mensajes salientes nunca se te devuelven como message.received, así que ese evento es siempre un mensaje real de otra persona. Antes de la V1.1 solo existía message.received: un manejador que ignora un type desconocido sigue funcionando sin cambios.