Pular para o conteúdo principal

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

CampoTipoObrigatórioDescrição
phoneIdUUIDsimPhone de origem (deve estar CONNECTED)
tostringsimDestinatário. Formato varia por canal (ver abaixo)
typeenumsimUm de: text, image, video, audio, document, sticker, location, contact, button, list, template, cta_url, location_request, address_message, flow
contentobjectsimSchema específico por tipo
quotedMessageIdstringnãoReply à mensagem original (use whatsappMessageId recebido em message:received ou retornado em messages_sent.whatsappMessageId)
idempotencyKeystringnão8-120 chars. Garante que reenvios com a mesma key não duplicam

Formato de to

CanalFormatoExemplo
BAILEYSE.164 sem + ou JID completo5511988887777 ou 5511988887777@s.whatsapp.net
WABAE.164 sem +, BSUID Meta, ou canonicalKey5511988887777 ou bsuid:BR.1234...
INSTAGRAMIGSID do contato1786543210...

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 202 com messageId.
  • Segunda chamada concorrente (ainda processando): retorna 202 com { status: "processing" }.
  • Após processado: retorna 200 com 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:

  1. Phone pertence à account → 404 PHONE_NOT_FOUND.
  2. Phone está CONNECTED → 409 PHONE_NOT_CONNECTED se não.
  3. 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?" }
}
CampoTipoLimite
textstring1-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ó!"
}
}'
CampoTipoLimite
mediaUrlURLprecisa ser HTTPS publicamente acessível
metaMediaIdstringID retornado por POST /v1/media/upload
captionstring1024 chars
filenamestring255 chars (relevante p/ document)
mimeTypestring120 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"
}
}
CampoTipoDescrição
mediaUrl ou metaMediaIdexatamente um
pttbooleantrue = nota de voz (push-to-talk); false = arquivo
mimeTypestringopcional

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"
}
}
CampoTipoObrigatórioLimite
latitudenumbersim-90 a 90
longitudenumbersim-180 a 180
namestringnão120 chars
addressstringnão255 chars

contact

Envia um ou mais contatos em formato vCard.

{
"type": "contact",
"content": {
"contacts": [
{ "name": "João Silva", "phone": "+5511988887777", "email": "joao@acme.com" }
]
}
}
CampoTipoLimite
contactsarray1-10
contacts[].namestring1-120 chars
contacts[].phonestring5-30 chars
contacts[].emailemailopcional

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" }
]
}
}
CampoTipoLimite
textstring1-1024
footerstring60 chars, opcional
buttonsarray1-3 itens
buttons[].idstring1-120 chars (callback que retorna em message:received)
buttons[].textstring1-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"
}
}
}
CampoTipoDescrição
namestringNome exato do template aprovado
languagestringpt_BR, en_US, etc. (default pt_BR)
variablesstring[]Substitui {{1}}, {{2}}, ... do body em ordem
headerMediaobjectPara templates com header IMAGE/VIDEO/DOCUMENT
headerMedia.typeenumimage, video, document
headerMedia.urlURLMídia HTTPS pública

Validações server-side:

  • Template precisa estar APPROVED no 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" }
}
}
CampoTipoObrigatórioLimite
textstringsim1-1024
footerstringnão60
buttonTextstringsim1-20
urlURLsim
header.typeenumnãotext, image, video, document
header.textstringsó se type=text60
header.mediaUrlURLsó 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" }
}
]
}
}
CampoTipoDefault
countryBR | INBR
valuesobjectpré-preenche campos (city, state, address, postalCode, etc)
savedAddressesarray (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"
}
}
CampoTipoDefaultDescrição
textstring1-1024 chars
flowIdstringID do flow publicado (ver Flows)
flowCtastringTexto do botão (1-20 chars)
flowActionenumnavigatenavigate (telas estáticas) ou data_exchange (backend orquestra)
flowTokenstringToken único pra rastrear sessão (até 120 chars)
flowActionPayload.screenstringTela inicial
flowActionPayload.dataobjectPré-popula campos da primeira tela
flowModeenumpublishedUse 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 WAMessageKey original consultando o cache (Redis TTL 7d) + tabela BaileysMessage (fallback DB). Se o cache expirou e a mensagem é muito antiga (90d+), o reply pode falhar — neste caso envie sem quotedMessageId.


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: QUEUEDSENTDELIVEREDREAD ou FAILED (terminal).

Errors

HTTPCodeQuando
404MESSAGE_NOT_FOUNDID 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

CampoTipoObrigatórioDescrição
scheduledAtISO datetimesimMínimo +5s do agora
payloadobjectsimMesmo 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

HTTPCodeQuando
400SCHEDULE_TOO_SOONscheduledAt < agora + 5s
429SCHEDULE_QUOTA_EXCEEDEDJá há 10k PENDING

GET /v1/messages/scheduled

Lista paginada.

Query

ParamTipoDefault
statusenum
fromISO datetime
toISO datetime
phoneIdUUID
limitint50 (máx 200)
cursorUUID

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

HTTPCodeQuando
409SCHEDULED_NOT_PENDINGStatus 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).