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

message
{
  "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.

POST/v1/messages
cURL
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." }'
201 Created
{
  "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
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." }'
201 Created
{
  "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.

subida multipart
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"
adjunto por URL
{
  "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.

TipoTipos de medioLímite
imageimage/jpeg image/png image/gif image/webp5 MB
videovideo/mp4 video/3gpp video/quicktime16 MB
audioaudio/mpeg audio/mp4 audio/aac audio/amr audio/ogg audio/wav16 MB
voice noteas audio, plus audio/opus audio/webm16 MB
documentapplication/pdf, the Office types, application/zip, application/json, text/plain, text/csv, text/vcard100 MB
stickerimage/webp500 KB

Descargar un adjunto

GET/v1/messages/:id/attachments/:attId

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.

POST/v1/messages/:id/reactions
DELETE/v1/messages/:id/reactions

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.

añadir una reacción
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

POST/v1/messages/:id/forward
DELETE/v1/messages/:id

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.

reenviar un mensaje
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.

ubicación y contactos
{
  "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" }
  ]
}

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

POST/v1/chats/:chatId/read
POST/v1/chats/:chatId/typing

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.

GET/v1/messages?channelId=chn_sales01&direction=INBOUND&limit=50
cURL
curl "https://api.sendveo.com/v1/messages?channelId=chn_sales01&direction=INBOUND&limit=50" \
  -H "X-API-KEY: sv_live_your_key_here"
Enviar y recibir mensajes de WhatsApp · Docs de Sendveo