Messages
A API de mensagens é canal-agnóstica: um único endpoint POST /v1/messages cobre todos os 15 tipos suportados, independentemente do phone.type (BAILEYS/WABA/INSTAGRAM). O gateway roteia internamente para o adapter certo.
Base path: /v1/messages
Modelo geral
Toda mensagem tem o mesmo envelope:
{
"phoneId": "<uuid do phone>",
"to": "<destinatário>",
"type": "<um dos 15 tipos>",
"content": { /* schema específico do tipo */ },
"quotedMessageId": "<opcional: whatsappMessageId de mensagem original>",
"idempotencyKey": "<opcional: string 8-120 chars>"
}
Campos comuns
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phoneId | UUID | sim | Phone de origem (deve estar CONNECTED) |
to | string | sim | Destinatário. Formato varia por canal (ver abaixo) |
type | enum | sim | Um de: text, image, video, audio, document, sticker, location, contact, button, list, template, cta_url, location_request, address_message, flow |
content | object | sim | Schema específico por tipo |
quotedMessageId | string | não | Reply à mensagem original (use whatsappMessageId recebido em message:received ou retornado em messages_sent.whatsappMessageId) |
idempotencyKey | string | não | 8-120 chars. Garante que reenvios com a mesma key não duplicam |
Formato de to
| Canal | Formato | Exemplo |
|---|---|---|
| BAILEYS | E.164 sem + ou JID completo | 5511988887777 ou 5511988887777@s.whatsapp.net |
| WABA | E.164 sem +, BSUID Meta, ou canonicalKey | 5511988887777 ou bsuid:BR.1234... |
| IGSID do contato | 1786543210... |
Para WABA, o gateway resolve automaticamente: se você manda BSUID e o phone correspondente está em cache (visto recentemente), prefere o phone (preserva a janela 30 dias Meta).
Idempotência
Inclua idempotencyKey em todas as chamadas críticas. O gateway usa Redis para garantir que requests com a mesma key + accountId só executem uma vez:
curl -X POST https://wpp.ogmma.com.br/v1/messages \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"phoneId": "5f7b2e1c-...",
"to": "5511988887777",
"type": "text",
"content": { "text": "OTP: 123456" },
"idempotencyKey": "user-12345-otp-2026-05-21-14"
}'
Comportamento por replay:
- Primeira chamada: processa normal, retorna
202commessageId. - Segunda chamada concorrente (ainda processando): retorna
202com{ status: "processing" }. - Após processado: retorna
200com o resultado salvo ({ messageId, whatsappMessageId, status: "SENT" }).
Recomendação: use UUID v4 derivado do contexto (usuário + ação + janela de tempo). TTL do registro de idempotência: configurável no servidor, default 24h.
POST /v1/messages
Enfileira uma mensagem para envio. Retorna imediatamente (202), o envio real acontece no Worker.
Validações antes do enqueue:
- Phone pertence à account → 404
PHONE_NOT_FOUND. - Phone está
CONNECTED→ 409PHONE_NOT_CONNECTEDse não. - Idempotência check (se key fornecida).
Response 202
{ "messageId": "9a1b2c3d-...", "status": "QUEUED" }
Você acompanha o status via GET /v1/messages/:id ou via webhook message:status (QUEUED → SENT → DELIVERED → READ ou FAILED).
Tipos de conteúdo
text
{
"phoneId": "...",
"to": "5511988887777",
"type": "text",
"content": { "text": "Olá! Como posso ajudar?" }
}
| Campo | Tipo | Limite |
|---|---|---|
text | string | 1-4096 chars |
image, video, document
Schema comum: MediaContentSchema. Forneça mediaUrl OU metaMediaId (exatamente um).
curl -X POST https://wpp.ogmma.com.br/v1/messages \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"phoneId": "...",
"to": "5511988887777",
"type": "image",
"content": {
"mediaUrl": "https://cdn.acme.com/foto.jpg",
"caption": "Veja só!"
}
}'
| Campo | Tipo | Limite |
|---|---|---|
mediaUrl | URL | precisa ser HTTPS publicamente acessível |
metaMediaId | string | ID retornado por POST /v1/media/upload |
caption | string | 1024 chars |
filename | string | 255 chars (relevante p/ document) |
mimeType | string | 120 chars |
Limites Meta: 5MB image, 16MB video, 100MB document. Para arquivos maiores em template header, use POST /v1/media/upload/resumable.
audio
{
"type": "audio",
"content": {
"mediaUrl": "https://cdn.acme.com/audio.ogg",
"ptt": true,
"mimeType": "audio/ogg; codecs=opus"
}
}
| Campo | Tipo | Descrição |
|---|---|---|
mediaUrl ou metaMediaId | — | exatamente um |
ptt | boolean | true = nota de voz (push-to-talk); false = arquivo |
mimeType | string | opcional |
sticker
{
"type": "sticker",
"content": { "mediaUrl": "https://cdn.acme.com/sticker.webp" }
}
Stickers devem ser WebP (estático ou animado).
location
{
"type": "location",
"content": {
"latitude": -23.5505,
"longitude": -46.6333,
"name": "Av. Paulista",
"address": "Av. Paulista, 1000 - São Paulo, SP"
}
}
| Campo | Tipo | Obrigatório | Limite |
|---|---|---|---|
latitude | number | sim | -90 a 90 |
longitude | number | sim | -180 a 180 |
name | string | não | 120 chars |
address | string | não | 255 chars |
contact
Envia um ou mais contatos em formato vCard.
{
"type": "contact",
"content": {
"contacts": [
{ "name": "João Silva", "phone": "+5511988887777", "email": "joao@acme.com" }
]
}
}
| Campo | Tipo | Limite |
|---|---|---|
contacts | array | 1-10 |
contacts[].name | string | 1-120 chars |
contacts[].phone | string | 5-30 chars |
contacts[].email | opcional |
button
Mensagem com até 3 botões de quick reply.
{
"type": "button",
"content": {
"text": "Como posso ajudar?",
"footer": "Atendimento Acme",
"buttons": [
{ "id": "menu_vendas", "text": "Comprar" },
{ "id": "menu_suporte", "text": "Suporte" }
]
}
}
| Campo | Tipo | Limite |
|---|---|---|
text | string | 1-1024 |
footer | string | 60 chars, opcional |
buttons | array | 1-3 itens |
buttons[].id | string | 1-120 chars (callback que retorna em message:received) |
buttons[].text | string | 1-20 chars |
list
Menu interativo com seções e linhas.
{
"type": "list",
"content": {
"text": "Escolha um serviço:",
"footer": "Disponíveis hoje",
"buttonText": "Ver opções",
"sections": [
{
"title": "Consultas",
"rows": [
{ "id": "consulta_clinica", "title": "Clínica geral", "description": "30min" },
{ "id": "consulta_pediatria", "title": "Pediatria", "description": "45min" }
]
}
]
}
}
Limites: text 1-1024, buttonText 1-20, sections 1-10, rows por seção 1-10, row.title 1-24, row.description 72 chars.
template
Template WABA aprovado. Pré-requisito: template criado e aprovado via POST /v1/templates (ver Templates).
{
"type": "template",
"content": {
"name": "boas_vindas_v2",
"language": "pt_BR",
"variables": ["João", "20%"],
"headerMedia": {
"type": "image",
"url": "https://cdn.acme.com/banner.jpg"
}
}
}
| Campo | Tipo | Descrição |
|---|---|---|
name | string | Nome exato do template aprovado |
language | string | pt_BR, en_US, etc. (default pt_BR) |
variables | string[] | Substitui {{1}}, {{2}}, ... do body em ordem |
headerMedia | object | Para templates com header IMAGE/VIDEO/DOCUMENT |
headerMedia.type | enum | image, video, document |
headerMedia.url | URL | Mídia HTTPS pública |
Validações server-side:
- Template precisa estar
APPROVEDno DB local → 400 se não. - Categoria (
MARKETING,UTILITY,AUTHENTICATION) é usada para cobrança de conversa WABA (24h window).
cta_url (call-to-action URL)
Mensagem com um único botão que abre URL no navegador. Mais simples que template para CTAs únicos.
{
"type": "cta_url",
"content": {
"text": "Aproveite nossa promoção exclusiva!",
"footer": "Válido até amanhã",
"buttonText": "Comprar agora",
"url": "https://acme.com/promo?ref=wpp",
"header": { "type": "image", "mediaUrl": "https://cdn.acme.com/banner.jpg" }
}
}
| Campo | Tipo | Obrigatório | Limite |
|---|---|---|---|
text | string | sim | 1-1024 |
footer | string | não | 60 |
buttonText | string | sim | 1-20 |
url | URL | sim | — |
header.type | enum | não | text, image, video, document |
header.text | string | só se type=text | 60 |
header.mediaUrl | URL | só se type≠text | — |
location_request
Solicita que o destinatário envie sua localização atual.
{
"type": "location_request",
"content": { "text": "Para calcular o frete, compartilhe sua localização." }
}
address_message
Solicita endereço estruturado (rua, número, CEP, etc). Disponível inicialmente para BR e IN.
{
"type": "address_message",
"content": {
"text": "Confirme seu endereço para entrega",
"country": "BR",
"values": { "city": "São Paulo", "state": "SP" },
"savedAddresses": [
{
"id": "casa",
"value": { "address": "Av Paulista, 1000", "city": "São Paulo", "state": "SP" }
}
]
}
}
| Campo | Tipo | Default |
|---|---|---|
country | BR | IN | BR |
values | object | pré-preenche campos (city, state, address, postalCode, etc) |
savedAddresses | array (até 5) | sugestões clicáveis |
flow
Abre um WhatsApp Flow (formulário nativo).
{
"type": "flow",
"content": {
"text": "Preencha seus dados para finalizar:",
"footer": "Acme Saúde",
"flowId": "1234567890",
"flowCta": "Iniciar",
"flowAction": "navigate",
"flowToken": "user-12345-form-abc",
"flowActionPayload": {
"screen": "PERSONAL_INFO",
"data": { "userName": "João" }
},
"flowMode": "published"
}
}
| Campo | Tipo | Default | Descrição |
|---|---|---|---|
text | string | — | 1-1024 chars |
flowId | string | — | ID do flow publicado (ver Flows) |
flowCta | string | — | Texto do botão (1-20 chars) |
flowAction | enum | navigate | navigate (telas estáticas) ou data_exchange (backend orquestra) |
flowToken | string | — | Token único pra rastrear sessão (até 120 chars) |
flowActionPayload.screen | string | — | Tela inicial |
flowActionPayload.data | object | — | Pré-popula campos da primeira tela |
flowMode | enum | published | Use draft só pra debug |
Reply (quoting)
Para responder uma mensagem específica, inclua quotedMessageId:
{
"phoneId": "...",
"to": "5511988887777",
"type": "text",
"content": { "text": "Sim, está disponível!" },
"quotedMessageId": "ABCD1234EFGH..."
}
quotedMessageId é o whatsappMessageId da mensagem original (você obtém via webhook message:received ou em messages_sent.whatsappMessageId).
Baileys-specific: o gateway reconstrói o
WAMessageKeyoriginal consultando o cache (Redis TTL 7d) + tabelaBaileysMessage(fallback DB). Se o cache expirou e a mensagem é muito antiga (90d+), o reply pode falhar — neste caso envie semquotedMessageId.
GET /v1/messages/:id
Consulta status de uma mensagem enviada por você.
curl https://wpp.ogmma.com.br/v1/messages/9a1b2c3d-... \
-H "X-API-Key: ak_live_..."
Response 200
{
"messageId": "9a1b2c3d-...",
"whatsappMessageId": "ABCD1234EFGH...",
"status": "READ",
"sentAt": "2026-05-21T14:00:00.000Z",
"deliveredAt": "2026-05-21T14:00:02.000Z",
"readAt": "2026-05-21T14:01:15.000Z",
"failedReason": null
}
Status possíveis: QUEUED → SENT → DELIVERED → READ ou FAILED (terminal).
Errors
| HTTP | Code | Quando |
|---|---|---|
| 404 | MESSAGE_NOT_FOUND | ID inexistente ou de outra account |
Nota: o gateway só persiste mensagens enviadas por você (tabela
messages_sent). Mensagens recebidas (message:received) não são persistidas em formato relacional — elas viram eventos no webhook. Se você quer histórico de inbound, salve no seu próprio banco quando processar o webhook.
Mensagens agendadas
Permite agendar envio futuro. O Worker faz polling do DB e enfileira no momento certo.
POST /v1/messages/scheduled
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
scheduledAt | ISO datetime | sim | Mínimo +5s do agora |
payload | object | sim | Mesmo body de POST /v1/messages |
curl -X POST https://wpp.ogmma.com.br/v1/messages/scheduled \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"scheduledAt": "2026-05-22T14:00:00.000Z",
"payload": {
"phoneId": "...",
"to": "5511988887777",
"type": "text",
"content": { "text": "Lembrete: sua consulta é amanhã às 10h." }
}
}'
Response 201
{
"id": "sch-1234-...",
"scheduledAt": "2026-05-22T14:00:00.000Z",
"status": "PENDING",
"createdAt": "2026-05-21T14:00:00.000Z"
}
Quota: máx 10.000 mensagens com status=PENDING por account.
Errors
| HTTP | Code | Quando |
|---|---|---|
| 400 | SCHEDULE_TOO_SOON | scheduledAt < agora + 5s |
| 429 | SCHEDULE_QUOTA_EXCEEDED | Já há 10k PENDING |
GET /v1/messages/scheduled
Lista paginada.
Query
| Param | Tipo | Default |
|---|---|---|
status | enum | — |
from | ISO datetime | — |
to | ISO datetime | — |
phoneId | UUID | — |
limit | int | 50 (máx 200) |
cursor | UUID | — |
Response: { items: [...], nextCursor: string | null }.
GET /v1/messages/scheduled/:id
Detalhe.
PATCH /v1/messages/scheduled/:id
Reagenda. Só funciona em status=PENDING.
Body: { "scheduledAt": "..." }
Errors
| HTTP | Code | Quando |
|---|---|---|
| 409 | SCHEDULED_NOT_PENDING | Status já é PROCESSING/SENT/FAILED/CANCELED |
DELETE /v1/messages/scheduled/:id
Cancela. Só em PENDING. Response 204.
Status lifecycle
POST /v1/messages
│
▼
QUEUED ────► (Worker pega da fila)
│
▼
SENT ────► (Meta/Baileys ack)
│
▼
DELIVERED ────► (cliente abriu WhatsApp)
│
▼
READ ────► (leu efetivamente)
Qualquer falha terminal → FAILED + failedReason
Cada transição publica um evento message:status no webhook do phone (ver Webhooks).