API da Ruvz
API REST para enviar e receber mensagens de WhatsApp e Instagram. A Ruvz é responsável pela integração com a Meta — habilitação do número, credenciais, formato de payload e reenvio.
A integração tem duas direções. O seu sistema chama esta API para enviar e consultar; a Ruvz chama a URL cadastrada por você a cada evento — mensagem recebida, mudança de status de entrega, conversa aberta ou encerrada. A segunda direção está descrita em Webhook.
Versão dos eventos: 2026-09-01 · Todos os corpos são JSON em UTF-8.
Duas restrições que definem a integração
1. Janela de atendimento de 24 horas
Restrição da Meta. No WhatsApp, mensagens de texto livre só são aceitas até 24 horas após a última mensagem recebida do contato. Fora da janela, o envio retorna 403 window_expired e o único envio possível é um modelo aprovado, pelo mesmo POST /v1/messages com template_name. No Instagram a janela é a mesma de 24 horas, mas não há modelos: fora dela, é preciso esperar o contato escrever de novo. Cada conversa retornada informa window_expires_at, o instante em que a janela se encerra.
2. O envio é assíncrono
200 em POST /v1/messages significa mensagem registrada e enfileirada, não entregue. A entrega é reportada posteriormente pelo evento message.status. Não use a resposta do envio como confirmação de entrega.
Autenticação
Autenticação por token, enviado no header Authorization em todos os requests. Tokens são criados em Configurações → Conta → API, no painel da Ruvz, e exibidos uma única vez no momento da criação.
Authorization: Bearer rvz_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCada token é restrito aos fluxos selecionados na criação e às permissões de leitura e de envio, concedidas separadamente. A validade é definida na criação; após expirar, o token é recusado.
A revogação tem efeito imediato no painel e em até 60 segundos nos servidores da API. Token revogado, expirado ou inexistente retorna 401 invalid_token, sem distinção entre os três casos.
Não há endpoint de criação de tokens. Para rotação sem interrupção: crie o novo token no painel, publique-o no seu sistema, valide o funcionamento e revogue o anterior.
Endpoints
| POST | /v1/messages | envio — texto, mídia ou modelo, um modo por request |
| GET | /v1/conversations | lista conversas de um número; com destinatário, retorna uma |
| GET | /v1/messages | histórico de uma conversa, do mais recente para o mais antigo |
| GET | /v1/contacts | lista contatos de um número; com destinatário, retorna um |
| POST | /v1/media | upload multipart; retorna media_id reutilizável |
| GET | /v1/media/:id | nova URL assinada para uma mídia |
Endereçamento
Nenhum endpoint recebe identificadores internos da Ruvz. Uma conversa é endereçada pelo par que as duas partes já possuem: o número conectado que envia e o identificador de quem recebe. Você não precisa armazenar identificadores da Ruvz no seu sistema.
| from | string · obrigatório | número conectado que envia, ou @usuario no Instagram. Aceita qualquer formatação: 5511988880000 e +55 11 98888-0000 são equivalentes |
| to | string | telefone do contato, em dígitos ou formatado |
| bsuid | string | identificador atribuído pela Meta quando o contato não expõe o telefone |
| instagram_id | string | identificador do contato no Instagram |
from é obrigatório em todos os requests, inclusive quando o token alcança um único fluxo. Exatamente um entre to, bsuid e instagram_id deve ser informado; o canal é derivado dessa escolha e nunca é declarado. Informar dois retorna 400 invalid_request.
Nas rotas de leitura, os mesmos campos são enviados como parâmetros de query. Os três identificadores são devolvidos nos eventos do webhook com os mesmos nomes.
Falhas de endereçamento
| 404 | unknown_sender | nenhum número conectado ao token corresponde a from |
| 404 | unknown_recipient | o destinatário nunca trocou mensagens com esse número |
| 404 | no_conversation | o contato existe, mas não há conversa neste canal — envie um modelo |
| 409 | ambiguous_recipient | mais de um cadastro corresponde ao identificador informado |
ambiguous_recipient ocorre quando dois cadastros do mesmo número existem na conta. O envio é recusado em vez de resolvido por escolha arbitrária. Solicite a unificação dos cadastros ao suporte.
Enviar uma mensagem
POST /v1/messages é o único endpoint de envio. Além do endereçamento, o corpo deve conter exatamente um modo de conteúdo. Dois modos no mesmo request retornam 400 invalid_request.
| text | string | texto livre. Sujeito à janela de 24 horas |
| media | string | URL https:// do arquivo, ou data URI em base64. Exclusivo com media_id |
| media_id | string | identificador devolvido por POST /v1/media. Exclusivo com media |
| template_name | string | nome de um modelo aprovado no fluxo. Único modo aceito fora da janela |
curl -X POST https://api.ruvz.com.br/v1/messages \
-H "Authorization: Bearer $RUVZ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f3c2b1a-..." \
-d '{
"from": "5511988880000",
"to": "5511999990000",
"text": "Seu pedido saiu para entrega."
}'{ "message_id": "msg_456", "status": "pending" }status na resposta é sempre pending. Os estados seguintes — sent, delivered, read, failed — chegam pelo evento message.status, correlacionados pelo message_id retornado aqui.
Enviar arquivos
Há três formas de informar o arquivo. Todas produzem o mesmo resultado: o arquivo é armazenado pela Ruvz antes do envio, o que garante que ele continue disponível na conversa, em reenvios e no media_id devolvido pelo webhook.
1. URL
Informe uma URL https:// pública. O download é feito pelo servidor, de forma assíncrona, após a resposta. Limite de 16MB.
curl -X POST https://api.ruvz.com.br/v1/messages \
-H "Authorization: Bearer $RUVZ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-8842-nf" \
-d '{
"from": "5511988880000",
"to": "5511999990000",
"media": "https://seu-sistema.com.br/notas/8842.pdf",
"caption": "Segue a nota fiscal",
"file_name": "nota-fiscal.pdf"
}'Como o download ocorre após a resposta, o 200 não confirma o acesso ao arquivo. URL indisponível, endereço de rede interna, arquivo acima do limite ou tipo não suportado resultam em message.status com failed e o motivo em error_message. Apenas https:// é aceito.
2. Base64
Informe o arquivo como data URI. Limite de 5MB já decodificados — acima disso, use URL ou upload, já que a codificação em base64 aumenta o corpo em aproximadamente um terço e é retransmitida a cada nova tentativa.
curl -X POST https://api.ruvz.com.br/v1/messages \
-H "Authorization: Bearer $RUVZ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: avatar-cliente-42" \
-d '{
"from": "5511988880000",
"to": "5511999990000",
"media": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
"media_type": "image",
"caption": "Seu comprovante"
}'3. Upload prévio
Para o mesmo arquivo enviado a vários destinatários, faça o upload uma vez em POST /v1/media (multipart, campos file e from) e reutilize o media_id retornado. Limite de 16MB.
curl -X POST https://api.ruvz.com.br/v1/messages \
-H "Authorization: Bearer $RUVZ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-8842-pdf" \
-d '{
"from": "5511988880000",
"to": "5511999990000",
"media_id": "YWNjXzEvZmxvdy...",
"file_name": "nota-fiscal.pdf",
"caption": "Segue a nota fiscal"
}'Campos e tipos
| media_type | string · opcional | image, video, audio ou document. Determinado pelo conteúdo do arquivo quando ausente |
| caption | string · opcional | legenda; não se aplica a áudio |
| file_name | string · opcional | nome exibido ao destinatário em documentos |
O tipo é determinado pelo conteúdo do arquivo, não pela extensão da URL nem pelo cabeçalho do data URI. Um media_type divergente do conteúdo retorna 400 media_type_mismatch nos modos com arquivo já disponível, e é corrigido silenciosamente no modo URL após o download.
Tipos aceitos: image/jpeg, image/png, image/webp, image/gif, video/mp4, audio/ogg, audio/mpeg, audio/mp4 e application/pdf.
Enviar um modelo
Modelos aprovados pela Meta são o único envio aceito fora da janela de 24 horas e a única forma de iniciar uma conversa com um número que nunca enviou mensagem. Quando não existe conversa, ela é criada por este request.
curl -X POST https://api.ruvz.com.br/v1/messages \
-H "Authorization: Bearer $RUVZ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": "5511988880000",
"to": "5511999990000",
"name": "Ana Souza",
"template_name": "confirmacao_pedido",
"parameters": ["8842"]
}'| template_name | string · obrigatório | nome do modelo aprovado neste fluxo |
| parameters | array de string | valores das variáveis, na ordem. A quantidade deve corresponder à do modelo |
| name | string | nome do contato. Obrigatório apenas no primeiro contato com o número |
Quantidade de parâmetros diferente da esperada retorna 400 invalid_template_parameters. Modelo inexistente ou não aprovado no fluxo retorna 403 template_not_approved.
Apenas to abre conversa. bsuid e instagram_id identificam participantes de conversas existentes e retornam 404 no_conversation quando não há uma.
Idempotência
Envie o header Idempotency-Key em todo envio. Uma chave repetida retorna a resposta original com 200 e o header Idempotent-Replay: true, sem novo envio. A chave é retornada no evento message.sent no campo client_reference.
Use uma chave distinta por mensagem. A validade é de 24 horas. Como o envio é um único endpoint, uma chave corresponde a um envio, independentemente do modo de conteúdo.
Requisição com a mesma chave ainda em processamento retorna 409 idempotency_in_flight. A mesma chave com um corpo diferente retorna 409 idempotency_key_reuse: uma chave corresponde a uma mensagem, e reaproveitá-la para outra resultaria no envio da segunda ser substituído pela resposta da primeira.
Em indisponibilidade do cache de idempotência, a verificação passa a aceitar todos os requests. Nesse intervalo, uma retransmissão pode resultar em envio duplicado — o comportamento é deliberado, já que recusar por precaução descartaria mensagens legítimas.
Consultar dados e arquivos
Todas as leituras usam a permissão messages:read e a mesma autenticação Bearer. Listagens retornam data e, quando houver outra página, next_cursor. Passe esse cursor na próxima chamada; a ordem é da mais recente para a mais antiga.
GET/v1/conversations
Liste as conversas de um número conectado. Acrescente to, bsuid ou instagram_id para obter uma conversa específica.
curl -G https://api.ruvz.com.br/v1/conversations \
-H "Authorization: Bearer $RUVZ_TOKEN" \
--data-urlencode "from=5511988880000" \
--data-urlencode "status=open" \
--data-urlencode "limit=50"{
"data": [{
"id": "cnv_123", "contact_id": "ct_123",
"to": "5511999990000", "channel": "whatsapp", "status": "open",
"window_expires_at": "2026-09-02T14:22:31Z",
"last_message_at": "2026-09-01T14:22:31Z",
"created_at": "2026-08-27T09:00:00Z"
}],
"next_cursor": "cnv_122"
}GET/v1/messages
Obtenha o histórico de uma conversa. Além de from, informe exatamente um destinatário.
curl -G https://api.ruvz.com.br/v1/messages \
-H "Authorization: Bearer $RUVZ_TOKEN" \
--data-urlencode "from=5511988880000" \
--data-urlencode "to=5511999990000" \
--data-urlencode "limit=50"{
"data": [{
"id": "msg_456", "conversation_id": "cnv_123",
"direction": "outbound", "type": "text", "text": "Seu pedido saiu.",
"status": "delivered", "origin": "agent",
"created_at": "2026-09-01T14:23:02Z"
}]
}GET/v1/contacts
Liste contatos associados ao número conectado. Use search para filtrar por texto, ou um destinatário para consultar um contato específico.
curl -G https://api.ruvz.com.br/v1/contacts \
-H "Authorization: Bearer $RUVZ_TOKEN" \
--data-urlencode "from=5511988880000" \
--data-urlencode "search=Ana" \
--data-urlencode "limit=50"{
"data": [{
"id": "ct_123", "name": "Ana Souza", "to": "5511999990000",
"phone": "5511999990000", "email": "ana@exemplo.com",
"blocked": false, "created_at": "2026-08-27T09:00:00Z"
}]
}POST/v1/media
Envie o arquivo uma vez e guarde o media_id retornado para usar em múltiplos envios. Aceita os tipos listados em Enviar arquivos, até 16MB.
curl -X POST https://api.ruvz.com.br/v1/media \
-H "Authorization: Bearer $RUVZ_TOKEN" \
-F "from=5511988880000" \
-F "file=@./nota-fiscal.pdf;type=application/pdf"{
"media_id": "YWNjXzEvZmxvdy...",
"mime_type": "application/pdf",
"file_name": "nota-fiscal.pdf",
"size_bytes": 184320
}GET/v1/media/:id
Gere uma URL assinada nova para uma mídia recebida no webhook ou enviada por upload. A URL vale por 24 horas.
curl https://api.ruvz.com.br/v1/media/YWNjXzEvZmxvdy... \
-H "Authorization: Bearer $RUVZ_TOKEN"{
"media_id": "YWNjXzEvZmxvdy...",
"url": "https://storage.googleapis.com/...",
"expires_at": "2026-09-02T14:22:31Z"
}Webhook
Cadastre a URL de recebimento em Fluxos → [fluxo] → Integração. A cada evento no fluxo, a Ruvz executa um POST nessa URL com um corpo JSON assinado.
Todos os eventos compartilham o mesmo envelope:
{
"api_version": "2026-09-01",
"event_id": "evt_msg_message.received_01J8...",
"delivery_id": "dlv_evt_msg_message.received_01J8..._1",
"type": "message.received",
"occurred_at": "2026-09-01T14:22:31.412Z",
"account_id": "acc_...",
"flow_id": "flow_...",
"data": { }
}| api_version | string | versão do formato, fixada no cadastro do webhook |
| event_id | string | estável entre tentativas do mesmo evento; use para descartar repetições |
| delivery_id | string | distinto a cada tentativa; informe em chamados de suporte |
| type | string | tipo do evento |
| occurred_at | ISO 8601 | instante do fato; ordene os eventos por este campo |
| data | object | conteúdo específico do tipo |
Responda 2xx em até 10 segundos e processe de forma assíncrona. Entregas sem resposta bem-sucedida são tentadas até 5 vezes, com intervalo crescente. Resposta 410 desativa o cadastro imediatamente. Após 100 falhas consecutivas, as entregas são pausadas e o estado é sinalizado no painel.
Catálogo de eventos
Estes são todos os eventos emitidos. Cada entrega usa o envelope de Webhook; os exemplos abaixo mostram apenas o objeto data para destacar o conteúdo específico.
message.receivedMensagem recebidamessage.sentMensagem enviadamessage.statusEntrega mudou de estadomessage.deletedMensagem apagadamessage.editedTexto alteradomessage.reactionReação adicionada ou removidacontact.createdContato criadocontact.updatedContato atualizadoconversation.openedConversa aberta ou reabertaconversation.closedConversa encerradamessage.received
Mensagem recebida do contato.
"data": {
"message_id": "01J8...", // identificador da mensagem na Ruvz
"from": "5511988880000", // número conectado que recebeu
"to": "5511999990000", // contato; devolva este par para responder
"bsuid": "BR.1A2B3C", // no lugar de "to" quando não há telefone
"conversation_id": "01J8...", // informativo; nenhum endpoint recebe este campo
"contact_id": "01J8...", // idem
"direction": "inbound",
"type": "text", // text | image | audio | video | document | sticker | location | template
"status": "received",
"origin": "contact",
"text": "Oi, meu pedido chegou?",
"created_at": "2026-09-01T14:22:31.412Z",
// Presentes quando aplicável:
"provider_message_id": "wamid.HBgN...", // identificador da Meta
"reply_to": "wamid.HBgN...", // mensagem citada pelo contato
"transcript": "...", // transcrição de áudio, quando habilitada
// Presentes em mensagens com mídia:
"media_id": "YWNjXzEvZmxvdy...",
"media_url": "https://storage.googleapis.com/...", // assinada, validade de 24h
"media_mime_type": "image/jpeg",
"media_file_name": "nota-fiscal.pdf"
}media_url é assinada e expira em 24 horas. Após a expiração, obtenha uma nova URL em GET /v1/media/:id a partir do media_id, que está sempre presente. Em falha de assinatura, o evento é entregue sem media_url.
message.sent
Mensagem enviada ao contato. Abrange envios por esta API, envios feitos na inbox da Ruvz e envios feitos pelo aplicativo WhatsApp Business.
"data": {
"message_id": "01J8...",
"from": "5511988880000",
"to": "5511999990000",
"conversation_id": "01J8...",
"contact_id": "01J8...",
"direction": "outbound",
"type": "text",
"status": "pending", // estado inicial; acompanhe em message.status
"origin": "api", // api | inbox | echo | campaign
"text": "Chegou sim!",
"created_at": "2026-09-01T14:23:02.100Z",
"client_reference": "pedido-8842" // Idempotency-Key informado no envio
}| api | origin | envio feito por esta API |
| inbox | origin | envio feito por um usuário na inbox da Ruvz |
| echo | origin | envio feito pelo aplicativo WhatsApp Business |
| campaign | origin | disparo de campanha feito por um usuário na Ruvz |
client_reference preenchido indica que a mensagem foi originada pelo seu sistema; atualize o registro existente em vez de criar outro. Ausente, a mensagem foi originada na Ruvz e corresponde a um registro novo.
No modo URL de mídia, o evento é emitido antes do download e não inclui media_id. Nos demais modos o campo está presente.
message.status
Mudança de estado de entrega. Emitido uma vez por estado; nem todos os estados ocorrem em toda mensagem.
"data": {
"message_id": "01J8...",
"conversation_id": "01J8...",
"status": "delivered", // sent | delivered | read | failed
"occurred_at": "2026-09-01T14:23:04.900Z",
// Presentes em failed:
"error_code": 131047,
"error_message": "Re-engagement message"
}message.deleted
Mensagem apagada. O conteúdo não é retransmitido, pois deixa de existir também na Ruvz.
"data": {
"message_id": "01J8...",
"conversation_id": "01J8...",
"deleted_by": "usr_..." // identificador de usuário da Ruvz | "contact" | "whatsapp-business-app"
}message.edited
Texto alterado dentro da janela de edição do WhatsApp. Não gera mensagem nova: atualize o texto do registro existente. Cada edição produz um evento.
"data": {
"message_id": "01J8...",
"conversation_id": "01J8...",
"text": "Chegou sim, hoje de manhã!",
"edited_by": "contact" // identificador de usuário da Ruvz | "contact" | "whatsapp-business-app"
}message.reaction
Reação adicionada ou removida. emoji vazio indica remoção, conforme a sinalização do próprio WhatsApp.
"data": {
"message_id": "01J8...",
"conversation_id": "01J8...",
"emoji": "👍",
"by": "usr_..." // identificador de usuário da Ruvz | "contact" | "whatsapp-business-app"
}contact.created · contact.updated
Primeiro contato de um número, ou alteração de cadastro. Mesmo formato para os dois tipos.
"data": {
"contact_id": "01J8...",
"name": "Ana Souza",
"to": "5511999990000", // mesmo valor de "phone", sob o nome usado no envio
"bsuid": "BR.1A2B3C", // quando não há telefone
"phone": "5511999990000", // WhatsApp
"instagram_id": "1784...", // Instagram; excludente com phone
"email": "ana@exemplo.com", // quando preenchido
"created_at": "2026-09-01T14:22:30.000Z"
}conversation.opened · conversation.closed
Abertura, reabertura e encerramento de conversa. O encerramento é normalmente automático, ao fim da janela de 24 horas sem novas mensagens.
"data": {
"conversation_id": "01J8...",
"from": "5511988880000",
"to": "5511999990000", // ou "bsuid" / "instagram_id"
"contact_id": "01J8...",
"channel": "whatsapp", // whatsapp | instagram
"status": "open", // open | closed
"window_expires_at": "2026-09-02T14:22:31.412Z" // ausente quando não há janela aberta
}conversation.opened também é emitido na reabertura de uma conversa encerrada.
Coexistência com o aplicativo WhatsApp Business
Um número pode operar simultaneamente nesta API e no aplicativo WhatsApp Business. Respostas enviadas pelo aplicativo são entregues como message.sent com origin: "echo". Integrações que ignoram esse evento exibem a conversa sem as respostas enviadas pelo aplicativo.
Consequências para a integração:
contact.createdeconversation.openedpodem ser originados por um envio do aplicativo, antes de qualquermessage.received.- Conversa iniciada pelo aplicativo não abre a janela de 24 horas — apenas mensagens recebidas do contato abrem. Até a resposta do contato, somente modelos aprovados são aceitos.
- Exclusão, reação e edição feitas no aplicativo são entregues como
message.deleted,message.reactionemessage.edited, com o autor identificado como"whatsapp-business-app"ou"contact".
Mensagens enviadas por esta API não são duplicadas por esse caminho: são reconhecidas pelo identificador do WhatsApp e não geram um segundo evento.
Ordem e duplicatas
Os eventos não são entregues em ordem garantida. A escolha é deliberada: uma fila ordenada bloqueia na primeira falha, e uma indisponibilidade momentânea do endpoint interromperia as entregas seguintes daquela conversa.
Ordene os eventos por occurred_at antes de exibi-los e registre os event_id já processados para descartar repetições. A entrega é ao-menos-uma-vez: o mesmo event_id pode ser recebido mais de uma vez.
Eventos de mensagem são autossuficientes: carregam o endereçamento completo. Uma integração que indexa por contact_id deve criar o registro na primeira ocorrência, sem depender de ter recebido contact.created antes.
Verificar a assinatura
Cada entrega leva estes headers. O HMAC-SHA256 é sobre a string "<timestamp>.<corpo cru>" — o timestamp entra na assinatura, e não só no header, justamente para que uma entrega capturada não sirva de replay para sempre.
X-Ruvz-Event: message.received
X-Ruvz-Delivery-Id: dlv_...
X-Ruvz-Timestamp: 1756738951
X-Ruvz-Signature-256: sha256=<hex>Rejeite o request se a diferença entre X-Ruvz-Timestamp e o seu relógio passar de 5 minutos, e compare em tempo constante.
Node.js
const crypto = require("crypto");
// IMPORTANTE: use o corpo CRU, antes de qualquer JSON.parse.
// express.json() já consumiu o stream — configure
// express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })
function verifyRuvz(rawBody, headers, secret) {
const ts = Number(headers["x-ruvz-timestamp"]);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(ts + "." )
.update(rawBody)
.digest("hex");
const got = headers["x-ruvz-signature-256"] || "";
const a = Buffer.from(expected);
const b = Buffer.from(got);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}PHP
<?php
function verify_ruvz(string $rawBody, array $headers, string $secret): bool {
$ts = (int) ($headers['X-Ruvz-Timestamp'] ?? 0);
if ($ts === 0 || abs(time() - $ts) > 300) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
$got = $headers['X-Ruvz-Signature-256'] ?? '';
return hash_equals($expected, $got);
}Erros
Erros retornam um código estável, destinado ao tratamento programático. A mensagem é descritiva e pode mudar.
{ "error": { "code": "window_expired", "message": "The customer service window has expired. Send an approved template instead." } }| 400 | invalid_request | corpo inválido, nenhum modo de conteúdo ou mais de um |
| 400 | invalid_phone | to não é um número em formato aceitável |
| 400 | invalid_media | media não é URL https:// nem data URI válido |
| 400 | invalid_media_id | media_id malformado |
| 400 | media_type_mismatch | media_type diverge do conteúdo do arquivo |
| 400 | unsupported_media_type | tipo de arquivo não aceito |
| 400 | invalid_template_parameters | quantidade de parâmetros diferente da esperada pelo modelo |
| 400 | name_required | primeiro contato com o número exige name |
| 401 | invalid_token | token inválido, revogado ou expirado |
| 403 | insufficient_scope | token sem a permissão exigida pelo endpoint |
| 403 | window_expired | fora da janela de atendimento; envie um modelo |
| 403 | template_not_approved | modelo inexistente ou não aprovado no fluxo |
| 403 | channel_disconnected | fluxo sem canal conectado |
| 404 | unknown_sender | from não corresponde a nenhum número do token |
| 404 | unknown_recipient | destinatário sem histórico com esse número |
| 404 | no_conversation | contato sem conversa neste canal |
| 404 | not_found | recurso inexistente ou fora dos fluxos do token |
| 409 | ambiguous_recipient | mais de um cadastro corresponde ao identificador |
| 409 | idempotency_in_flight | request anterior com a mesma chave em processamento |
| 409 | idempotency_key_reuse | mesma chave com corpo diferente do request original |
| 413 | file_too_large | arquivo acima do limite do modo utilizado |
| 400 | invalid_idempotency_key | Idempotency-Key fora do formato aceito |
| 403 | token_misconfigured | token sem fluxo algum atribuído; gere um novo no painel |
| 422 | unsupported_channel | operação não existe neste canal — modelos são exclusivos do WhatsApp |
| 429 | rate_limited | limite de requisições do token excedido |
| 500 | internal_error | falha nossa; a mensagem não foi registrada, pode repetir com a mesma Idempotency-Key |
| 503 | storage_unavailable | armazenamento de mídia indisponível; repita depois |
404 é também a resposta para recursos existentes em fluxos que o token não alcança: 403 confirmaria a existência do recurso.
Limites
500 requisições por segundo por token, com pico de 1000. O limite é por token e não por conta, o que permite isolar cargas: um token de relatórios não consome a capacidade do token de envio.
Esse limite protege a infraestrutura da API e não corresponde à capacidade de envio. A capacidade de envio é definida pela Meta, por número de WhatsApp, e é respeitada pela Ruvz antes da entrega. Contas com muitos números conectados têm capacidade agregada superior a esse limite; solicite ajuste ao suporte se necessário.
Excedido o limite, a resposta é 429 rate_limited e nenhuma mensagem é enviada.
| 16MB | upload e URL | POST /v1/media e media como URL |
| 5MB | base64 | tamanho já decodificado, em media como data URI |
| 24h | media_url | validade da URL assinada nos eventos |
| 24h | Idempotency-Key | janela de reconhecimento de chave repetida |
Listagens são paginadas por cursor. Informe limit (padrão 50, máximo 200) e repita a chamada com o next_cursor retornado. Ausência de next_cursor indica fim da coleção.
Changelog
- 2026-09-01
- Versão inicial. Endpoint único de envio para texto, mídia (URL, base64 ou upload) e modelos; leitura de conversas, mensagens e contatos; webhook com dez tipos de evento.
A versão dos eventos é fixada no cadastro do webhook. Alterações de formato são publicadas como versão nova; o endpoint cadastrado continua recebendo a versão anterior até a atualização do cadastro.