Mensajes
Lee los mensajes entrantes que reciben tus números y envía los tuyos. Responde a una conversación que inició un contacto o escribe directamente a un número de teléfono: Sendveo abre la conversación por ti.
El objeto mensaje
{
"id": "msg_7Qk2",
"channelId": "chn_sales01",
"direction": "INBOUND",
"chatId": "chat_aa11",
"from": "+212670111222",
"to": null,
"senderName": "Khadija Alaoui",
"text": "Here is the model we agreed on.",
"type": "image",
"status": "RECEIVED",
"statusAt": null,
"failureReason": null,
"timestamp": "2026-09-09T14:02:11Z",
"attachments": [
{
"id": "att_5Hq1",
"kind": "IMAGE",
"mimeType": "image/jpeg",
"fileName": "modele.jpg",
"size": 184320,
"durationSeconds": null,
"width": 1080,
"height": 1440,
"voiceNote": false,
"url": "https://api.sendveo.com/v1/messages/msg_7Qk2/attachments/att_5Hq1"
}
],
"replyTo": { "id": "msg_7Qk1", "providerMessageId": "wamid.HB1" },
"location": null,
"contacts": null,
"linkPreview": null,
"reactions": [{ "emoji": "👍", "by": "+212670111222", "at": "2026-09-09T14:03:00Z" }],
"deletedAt": null,
"editedAt": null
}direction es INBOUND u OUTBOUND. from lleva el remitente en los mensajes entrantes y es null en los salientes; to es su reflejo: lleva el número del destinatario en un mensaje que enviaste a un número de teléfono y es null en los demás casos. status es SENT o FAILED en los mensajes salientes; los entrantes son siempre RECEIVED.
Enviar un mensaje
Cada envío indica el número emisor en channelId (numberId se acepta como el mismo campo), el mensaje en text y exactamente un destinatario: chatId para responder a una conversación existente, o to para un número de teléfono. El número debe estar CONNECTED; de lo contrario, la solicitud devuelve 409.
curl -X POST https://api.sendveo.com/v1/messages \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "channelId": "chn_sales01", "chatId": "chat_aa11", "text": "Yes - shipping tomorrow." }'{
"message": {
"id": "msg_8Rm5",
"channelId": "chn_sales01",
"direction": "OUTBOUND",
"chatId": "chat_aa11",
"to": null,
"text": "Yes - shipping tomorrow.",
"status": "SENT",
"timestamp": "2026-09-09T14:05:40Z"
}
}Enviar a un número de teléfono
Usa to con un número de teléfono en formato E.164 para escribir a alguien que nunca te ha escrito: Sendveo abre la conversación y devuelve su chatId, así que las respuestas llegan al mismo hilo. +212661234567, 212661234567 y 00212661234567 se aceptan por igual, con o sin espacios, guiones o paréntesis.
curl -X POST https://api.sendveo.com/v1/messages \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "channelId": "chn_sales01", "to": "+212661234567", "text": "Your order is ready." }'{
"message": {
"id": "msg_9Tn7",
"channelId": "chn_sales01",
"direction": "OUTBOUND",
"chatId": "chat_9Fp2",
"to": "+212661234567",
"text": "Your order is ready.",
"status": "SENT",
"timestamp": "2026-09-15T09:12:03Z"
}
}Enviar a un número que ya tiene una conversación continúa esa conversación en lugar de abrir una segunda, tanto si escribió primero el contacto como si lo hiciste tú. Una misma persona nunca acaba repartida en dos hilos.
Un to que no sea un número de teléfono utilizable, chatId y to juntos, o ninguno de los dos: en los tres casos la respuesta es 400 VALIDATION_FAILED, con el campo nombrado en details, y no se envía nada. Una forma nacional que empieza por cero, como 0661234567, se rechaza: sin prefijo de país es una suposición.
Sé moderado con los primeros contactos: WhatsApp bloquea los números que escriben a mucha gente que nunca escribió primero, y la que acaba bloqueada es tu propia línea de empresa. Sendveo permite 20 conversaciones nuevas por número y hora, y a partir de ahí responde 429 OUTBOUND_LIMIT con un Retry-After. Las respuestas nunca se cuentan.
Adjuntos
Un mensaje puede llevar hasta 10 archivos. Envía los bytes como multipart/form-data, o pasa una url pública en JSON y Sendveo descarga el archivo por ti. Los archivos y las URL no se pueden mezclar en un mismo mensaje. text es opcional cuando el mensaje lleva un archivo: una foto sin pie es un mensaje real.
En una solicitud multipart los archivos van en attachments (repite el campo para enviar varios), y un archivo de audio que quieras mostrar como onda en línea va en voice_note. Cualquier otro nombre de campo de archivo devuelve 400.
curl -X POST https://api.sendveo.com/v1/messages \
-H "X-API-KEY: sv_live_your_key_here" \
-F channelId=chn_sales01 \
-F to=+212661234567 \
-F "text=Voici le devis" \
-F "attachments=@devis.pdf;type=application/pdf"{
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"attachments": [
{
"url": "https://cdn.example.com/devis.pdf",
"mimeType": "application/pdf",
"filename": "devis.pdf"
}
]
}Límites
Son los límites de WhatsApp, no los nuestros: un archivo que los supera lo rechaza WhatsApp después de la subida, así que Sendveo lo rechaza antes y nombra el límite. Un tipo que no esté en la lista se rechaza por su nombre. Un error de adjunto es un 400 VALIDATION_FAILED con details[0].path = "attachments.N.…", de modo que quien envía cinco archivos sabe cuál fue rechazado, y nada llega al servicio de mensajería.
| Tipo | Tipos de medio | Límite |
|---|---|---|
| image | image/jpeg image/png image/gif image/webp | 5 MB |
| video | video/mp4 video/3gpp video/quicktime | 16 MB |
| audio | audio/mpeg audio/mp4 audio/aac audio/amr audio/ogg audio/wav | 16 MB |
| voice note | as audio, plus audio/opus audio/webm | 16 MB |
| document | application/pdf, the Office types, application/zip, application/json, text/plain, text/csv, text/vcard | 100 MB |
| sticker | image/webp | 500 KB |
Descargar un adjunto
El endpoint devuelve los bytes, con la misma autenticación que el resto de la API. Es un proxy, nunca una redirección. Sendveo no almacena ningún byte: el archivo se descarga bajo demanda, se transmite y se olvida, así que cada descarga es una llamada nueva y un adjunto solo se puede descargar mientras el servicio de mensajería lo conserve. Un archivo que enviaste tú no se puede volver a descargar (409): sus metadatos están en el mensaje para que tu propia interfaz muestre lo que enviaste.
Respuestas y reacciones
Cita un mensaje con replyTo: un id de Sendveo con la forma msg_…, o el id del proveedor de un mensaje anterior a la conexión, que se acepta y se transmite tal cual. En ese segundo caso, el mensaje que recibes lleva replyTo con id: null.
Una reacción es un único emoji nativo. El texto corriente devuelve 400. Volver a reaccionar sustituye tu reacción, nunca añade una segunda, y { "emoji": null } (o DELETE …/reactions, la misma operación) la retira. El campo reactions de un mensaje son las reacciones vigentes ahora mismo, no un registro; la reacción del propio número conectado aparece con by: null.
curl -X POST https://api.sendveo.com/v1/messages/msg_8Rm5/reactions \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "emoji": "👍" }'Reenvío y eliminación
Un reenvío responde 201 con la copia guardada en la conversación de destino, así que tu propia bandeja de entrada muestra lo que enviaste. { to: "+212661234567" } reenvía dentro de la conversación que ya tienes con ese número; un reenvío a un número con el que no hay conversación todavía devuelve 409, no una conversación nueva: convertir un reenvío en un primer contacto en frío gastaría la reputación del número sin que lo hayas pedido.
curl -X POST https://api.sendveo.com/v1/messages/msg_8Rm5/forward \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "chatId": "chat_aa11" }'Eliminar retira el mensaje en WhatsApp. Del lado de Sendveo la fila se conserva: text se vacía, attachments pasa a [] y se rellena deletedAt, de modo que el historial sigue mostrando que se dijo algo y se retiró. Eliminar un mensaje ya eliminado devuelve 409.
Reaccionar, reenviar, eliminar para todos y marcar como leído requieren el ámbito messages:manage, que deliberadamente NO forma parte de messages:send: actuar sobre un mensaje que ya existe es una autoridad distinta de escribir uno nuevo. Las claves emitidas antes de que existiera este ámbito no lo llevan y responden 403: emite una clave nueva en lugar de editar la antigua. POST /v1/chats/:chatId/typing solo necesita messages:send.
Ubicaciones y contactos
Una ubicación se guarda de forma estructurada y se devuelve como números en ambos sentidos. Un valor fuera de rango devuelve 400 nombrando location.latitude o location.longitude. Tu text se mantiene encima.
Cada teléfono de una tarjeta de contacto se normaliza con la misma regla que un destinatario, y una forma nacional que empieza por cero se rechaza. Las tarjetas de contacto entrantes se analizan y te llegan como nombres y números en E.164. Una ubicación y unos contactos no pueden viajar en el mismo mensaje.
{
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"text": "On est ici",
"location": {
"latitude": 33.5731,
"longitude": -7.5898,
"name": "Casablanca",
"address": "Boulevard Zerktouni"
}
}
{
"channelId": "chn_sales01",
"chatId": "chat_aa11",
"contacts": [
{ "name": "Nadia Benali", "phones": ["+212661778899"], "organisation": "Atlas" }
]
}Vistas previas de enlaces
linkPreview vale true por defecto: el primer enlace http(s) del texto vuelve como linkPreview: { url }, así no tienes que volver a analizar el cuerpo. Sendveo nunca descarga el enlace: ni título, ni imagen, ni una sola petición saliente por envío.
Lo que la opción no hace: la tarjeta de vista previa que ve un destinatario la dibuja su cliente de WhatsApp a partir de la URL del texto, y no existe ninguna primitiva para suprimirla. linkPreview: false solo impide que Sendveo extraiga el enlace; no suprime la tarjeta de WhatsApp, y el texto nunca se reescribe para intentarlo.
Marcar como leído y escribiendo
Marcar una conversación como leída y mostrar el indicador de escritura son, cada uno, una sola llamada. El indicador de escritura es de mejor esfuerzo por contrato: responde 200 { "sent": false } cuando el servicio de mensajería lo rechaza, en lugar de hacer fallar tu solicitud; una cortesía no debe hacer fallar a quien llama. Lo que sigue siendo un error es tu propio fallo: una conversación desconocida (404), un número que no está conectado (409), una duración fuera de 1-25000 ms (400).
Estado de entrega
En un mensaje saliente, status recorre SENT → DELIVERED → READ, y solo avanza hacia delante: los acuses llegan desordenados, y un DELIVERED tardío nunca deja un mensaje como no leído. FAILED gana a todo y nada le gana a él. statusAt es cuándo cambió por última vez, y failureReason solo se rellena junto con FAILED.
Listar mensajes
Filtra por número, dirección y hora. Los resultados aparecen del más reciente al más antiguo y se limitan a 200 por página.
curl "https://api.sendveo.com/v1/messages?channelId=chn_sales01&direction=INBOUND&limit=50" \
-H "X-API-KEY: sv_live_your_key_here"