API Unred — WhatsApp oficial

Envie e receba mensagens de WhatsApp a partir do seu sistema, pela API oficial da Meta.

Feita para sistemas de gestão (ERP), inclusive de autopeças. A Unred é Tech Provider oficial da Meta e já tem integração com Jacsys, AutoBaze, TOTVS, LJ Sistemas, Z-PRO, TCar · Tecinco, CNE Sistemas. Ver integrações

Visão geral

A API da Unred conecta o seu sistema (ERP, CRM, loja, sistema próprio) ao WhatsApp pela API oficial da Meta (WhatsApp Cloud API). A Unred cuida do número, da conexão com a Meta, dos arquivos, do reenvio de avisos e do histórico; o seu sistema só chama endpoints simples em JSON.

  • Base: https://api.unred.com.br/v1 (também responde em https://unred.com.br/api/v1)
  • Formato: JSON em UTF-8, campos em snake_case, datas em ISO-8601 (UTC), telefones com DDI só com números (5511999990000).
  • Contrato completo: OpenAPI 3.1 — importe no Postman, Insomnia ou gere um cliente em qualquer linguagem.
  • Tudo também aparece no Unred: cada mensagem enviada ou recebida pela API fica no chat do Unred, então a sua equipe pode acompanhar e responder por lá.

Quem paga o quê. A Meta cobra as mensagens dela direto no cartão cadastrado na sua conta do Facebook (Meta Business), pela tabela oficial dela, sem margem nossa. A Unred cobra a mensalidade do plano. A API mostra a categoria e se a Meta cobra cada mensagem (pricing).

Começar em 5 minutos

1. Crie a chave. No Unred: Configurações › Conexão com seu sistema › Nova chave. A chave aparece uma vez — guarde no cofre de segredos do seu sistema.

2. Mande a primeira mensagem. Pra quem ainda não falou com você nas últimas 24h, o WhatsApp só aceita template aprovado:

curl https://api.unred.com.br/v1/messages \
  -H "Authorization: Bearer unr_live_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-48213-aviso" \
  -d '{
    "to": "5511999990000",
    "type": "template",
    "template": {
      "name": "pedido_confirmado",
      "language": "pt_BR",
      "components": [
        { "type": "body", "parameters": [
          { "type": "text", "text": "Maria" },
          { "type": "text", "text": "48213" }
        ]}
      ]
    },
    "client_reference": "pedido-48213"
  }'

A resposta volta na hora (202) com o id e "status": "queued":

{ "id": "cm2x8k1q50001", "object": "message", "direction": "outbound", "status": "queued",
  "type": "template", "contact": { "phone": "5511999990000", "name": null },
  "client_reference": "pedido-48213", "wamid": null, "created_at": "2026-10-09T13:00:00.000Z" }

3. Receba o resultado. Cadastre um webhook (tela ou POST /v1/webhooks) e você recebe message.sent, message.delivered, message.read ou message.failed — e tudo que a pessoa responder, em message.received.

Quer testar sem enviar nada? Use uma chave unr_test_…: as respostas e os webhooks são iguais (com "livemode": false), e nenhuma mensagem sai.

Autenticação e chaves

Toda chamada leva o cabeçalho:

Authorization: Bearer unr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PrefixoO que faz
unr_live_Envia de verdade pela Meta.
unr_test_Não envia nada e não lê nem mexe em dado real. Simula o envio (POST /v1/messages, com webhooks livemode: false), consulta as próprias mensagens de teste, lista templates e números e simula o descadastro. O resto (listar mensagens reais, relatório, webhooks, mídia, criar ou apagar template) responde 403: use a chave de produção.
  • Guardamos só uma impressão (hash) da chave — se perder, crie outra e revogue a antiga.
  • Revogar é imediato: a próxima chamada com ela já recebe 401.
  • Cada resposta traz Request-Id. Mande esse valor se precisar de suporte.

Enviar mensagens

POST /v1/messages aceita todo tipo que a API oficial do WhatsApp aceita, no mesmo formato da Meta: você informa type e o objeto de mesmo nome. Campos novos que a Meta lançar passam direto.

A janela de 24 horas. Depois que a pessoa manda uma mensagem, você pode responder com qualquer tipo por 24h. Fora disso, só template aprovado — a API recusa na hora com outside_customer_service_window, em vez de deixar falhar depois.

Campos comuns: to (obrigatório), type (obrigatório), channel_id (só se a conta tiver mais de um número), reply_to (id da mensagem citada), client_reference (seu identificador, volta em todo webhook), contact_name (nome, se a pessoa ainda não existir).

Texto

{ "to": "5511999990000", "type": "text", "text": { "body": "Seu pedido saiu para entrega 🚚", "preview_url": true } }

Template com imagem no cabeçalho e botão de link

{ "to": "5511999990000", "type": "template", "template": {
  "name": "boleto_disponivel", "language": "pt_BR",
  "components": [
    { "type": "header", "parameters": [ { "type": "image", "image": { "link": "https://seu-site.com/banner.png" } } ] },
    { "type": "body", "parameters": [ { "type": "text", "text": "R$ 189,90" } ] },
    { "type": "button", "sub_type": "url", "index": "0", "parameters": [ { "type": "text", "text": "abc123" } ] }
  ] } }

Parâmetros com nome ("parameter_name": "cliente"), botão de copiar código, carrossel e oferta por tempo limitado seguem exatamente o formato da Meta.

Imagem, vídeo, áudio, documento, figurinha

{ "to": "5511999990000", "type": "document", "document": { "link": "https://seu-site.com/nota-48213.pdf", "filename": "Nota 48213.pdf", "caption": "Sua nota fiscal" } }

Use o seu próprio link HTTPS ou suba o arquivo em POST /v1/media. Áudio com "voice": true (OGG/Opus) aparece como mensagem de voz.

Localização

{ "to": "5511999990000", "type": "location", "location": { "latitude": -23.5614, "longitude": -46.6559, "name": "Loja Paulista", "address": "Av. Paulista, 1000" } }

Contato (cartão)

{ "to": "5511999990000", "type": "contacts", "contacts": [ { "name": { "formatted_name": "Suporte", "first_name": "Suporte" }, "phones": [ { "phone": "+551130000000", "type": "WORK" } ] } ] }

Botões (até 3)

{ "to": "5511999990000", "type": "interactive", "interactive": {
  "type": "button", "body": { "text": "Confirma a entrega amanhã?" },
  "action": { "buttons": [
    { "type": "reply", "reply": { "id": "confirmar-48213", "title": "Confirmo" } },
    { "type": "reply", "reply": { "id": "remarcar-48213", "title": "Remarcar" } } ] } } }

Quando a pessoa toca, chega message.received com button_reply_id: "confirmar-48213".

Lista, botão de link e outros interativos — "type": "list", "cta_url", "flow", "product", "product_list", "catalog_message", "location_request_message": mesmo formato da Meta.

Responder citando — "reply_to": "<id da mensagem>" em qualquer envio.

Reação

{ "to": "5511999990000", "type": "reaction", "reaction": { "message_id": "cm2x8k1q50001", "emoji": "👍" } }

Emoji vazio tira a reação. A resposta é 200 (reação não vira mensagem nova).

Marcar como lida — POST /v1/messages/{id}/read com { "typing_indicator": true } mostra o tique azul e "digitando…" até a sua resposta sair.

Receber mensagens (webhooks)

Cadastre a URL do seu sistema (HTTPS) e escolha os eventos — "*" assina todos. Cada evento chega como POST:

{
  "id": "evt_3f2a9c0e8b7d4c1a9e6f5b4a3c2d1e0f",
  "object": "event",
  "type": "message.received",
  "api_version": "v1",
  "created_at": "2026-10-09T13:05:12.000Z",
  "livemode": true,
  "data": {
    "id": "cm2x8m3r90007", "object": "message", "direction": "inbound", "status": "received",
    "channel_id": "cm1abc...", "type": "image",
    "contact": { "phone": "5511999990000", "name": "Maria" },
    "text": "Segue o comprovante",
    "media": { "url": "https://media.unred.com.br/...", "mime_type": "image/jpeg", "file_name": null, "size_bytes": 182344 },
    "button_reply_id": null, "reply_to": null, "wamid": "wamid.HBgN...",
    "meta": { "...": "o objeto ORIGINAL que a Meta mandou" }
  }
}
  • Mídia com link permanente. O link da Meta vence em 5 minutos; o media.url que mandamos não vence. O evento de uma mensagem com arquivo sai quando o arquivo já está guardado (normalmente segundos).
  • Nada se perde. data.meta traz o objeto original da Meta — formulário (Flow), pedido de catálogo, aviso de troca de número, tudo.
  • Responda 2xx em até 10 segundos. Processe depois (fila). Qualquer outra resposta faz a gente reenviar.
EventoQuando chega
message.receivedA pessoa mandou uma mensagem (texto, mídia, localização, contato, toque em botão, formulário, pedido…). Mídia chega com link permanente.
message.reactionA pessoa reagiu (ou tirou a reação) a uma mensagem. emoji vazio = tirou.
message.editedA pessoa editou uma mensagem que tinha mandado. text é a versão nova.
message.deletedA pessoa apagou uma mensagem para todos.
message.sentA Meta aceitou a mensagem e ela saiu.
message.deliveredA mensagem chegou no celular da pessoa.
message.readA pessoa leu (só quando ela deixa a confirmação de leitura ligada).
message.failedA mensagem não foi entregue. error.code traz o código da Meta.
template.status_updatedUm template foi aprovado, recusado, pausado ou desativado pela Meta.
template.category_updatedA Meta mudou a categoria de um template — isso muda o preço dele.
template.quality_updatedA nota de qualidade de um template mudou.
template.components_updatedO conteúdo de um template foi alterado.
channel.updatedMudou algo no número: qualidade, nome de exibição, limite de envio ou alerta da conta.
contact.marketing_preference_updatedA pessoa parou (ou voltou a aceitar) mensagens de marketing pelo próprio WhatsApp.

Eventos message.* trazem o objeto Mensagem em data. Os de template, número e preferência trazem { "meta": <aviso original da Meta> }.

Status e cobrança de cada mensagem

queued (na fila) → sent (a Meta aceitou) → delivered (chegou no celular) → read (leu, se a pessoa deixa a confirmação ligada). Ou failed, com error.code da Meta (ex.: 131026 número sem WhatsApp, 131047 fora da janela de 24h, 131049 limite de marketing por pessoa).

Toda mensagem traz pricing, informado pela própria Meta:

"pricing": { "category": "utility", "billable": true }

billable: false = a Meta não cobra (ex.: respostas dentro da janela de 24h). É a mesma conta da fatura da Meta.

Conferir a assinatura do webhook

Todo evento vem assinado com o segredo do webhook (whsec_…, mostrado só na criação):

Unred-Signature: t=1760015112,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 = HMAC-SHA256 em hexadecimal de "<t>.<corpo bruto>". Recuse se a conta não bater ou se t tiver mais de 5 minutos.

Node.js

import crypto from "node:crypto"
function assinaturaValida(corpoBruto, cabecalho, segredo) {
  const p = Object.fromEntries(cabecalho.split(",").map((x) => x.split("=")))
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false
  const esperado = crypto.createHmac("sha256", segredo).update(`${p.t}.${corpoBruto}`).digest("hex")
  const a = Buffer.from(esperado)
  const b = Buffer.from(p.v1 ?? "")
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Python

import hmac, hashlib, time
def assinatura_valida(corpo_bruto: bytes, cabecalho: str, segredo: str) -> bool:
    p = dict(x.split("=", 1) for x in cabecalho.split(","))
    if abs(time.time() - int(p["t"])) > 300:
        return False
    esperado = hmac.new(segredo.encode(), f'{p["t"]}.'.encode() + corpo_bruto, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, p.get("v1", ""))

PHP

function assinaturaValida(string $corpo, string $cabecalho, string $segredo): bool {
  parse_str(str_replace(',', '&', $cabecalho), $p);
  if (abs(time() - (int)$p['t']) > 300) return false;
  $esperado = hash_hmac('sha256', $p['t'] . '.' . $corpo, $segredo);
  return hash_equals($esperado, $p['v1'] ?? '');
}

Use o corpo bruto (os bytes exatos recebidos), não o JSON já convertido.

Reenvio, repetição e duplicados

  • A gente reenvia. Se o seu servidor não responder 2xx, o evento volta em 1 min, 5 min, 25 min, 2 h e 10 h. O histórico fica em GET /v1/webhooks/{id}/deliveries, e dá pra reenviar na hora (…/deliveries/{id}/retry).
  • Você pode repetir sem medo. Mande Idempotency-Key em todo POST: a mesma chave (por 24h) devolve a MESMA resposta, sem enviar de novo — com o cabeçalho Idempotent-Replayed: true. Use algo do seu lado, como pedido-48213-aviso.
  • Ignore repetidos. Cada evento tem id único (evt_…) — guarde e descarte se vier de novo.
  • Ordem não é garantida. delivered pode chegar antes de sent. Use os horários (sent_at, delivered_at, read_at) do objeto, não a ordem de chegada.

Templates

  • GET /v1/templates — lista direto da Meta (status, categoria, qualidade, componentes).
  • POST /v1/templates — submete pra aprovação, no formato da Meta. O resultado chega por template.status_updated.
  • DELETE /v1/templates/{name} — apaga (todos os idiomas, ou um com hsm_id).
{ "name": "pedido_confirmado", "language": "pt_BR", "category": "UTILITY",
  "components": [ { "type": "BODY", "text": "Oi {{1}}, seu pedido {{2}} foi confirmado.",
                    "example": { "body_text": [ [ "Maria", "48213" ] ] } } ] }

Categoria decide o preço. A Meta cobra marketing bem mais caro que utilidade (aviso de pedido, boleto, entrega). Se ela reclassificar um template, você recebe template.category_updated.

Mídia

POST /v1/media (multipart/form-data, campo file) guarda o arquivo e devolve um url permanente pra usar em qualquer envio — suba uma vez e mande pra quantas pessoas quiser.

TipoFormatosMáximo
ImagemJPEG, PNG5 MB
ÁudioAAC, AMR, MP3, M4A, OGG (Opus)16 MB
VídeoMP4, 3GP (H.264 + AAC)16 MB
DocumentoPDF, DOC(X), XLS(X), PPT(X), TXT50 MB pelo upload (até 100 MB pelo seu próprio link)
FigurinhaWebP100 KB (animada: 500 KB)

Descadastro de marketing

Quem pediu pra não receber propaganda não recebe template de marketing pela API (recipient_opted_out). Avisos de utilidade e autenticação continuam.

  • A marca nasce sozinha quando a pessoa toca em "parar promoções" no WhatsApp (evento contact.marketing_preference_updated).
  • POST /v1/marketing-opt-outs { "phone": "5511999990000" } descadastra; DELETE /v1/marketing-opt-outs/{phone} desfaz; GET lista.

Uso do mês

GET /v1/usage?month=2026-10 — a mesma conta da tela do Unred e da sua fatura.

  • counted = mensagens enviadas pela API que a Meta aceitou no mês (falhas não contam; mensagens recebidas e respostas da equipe também não).
  • franchise / remaining / over_franchise = quanto do plano já foi usado.
  • meta_billable_by_category = o que a Meta cobra, por categoria.
  • by_day e top_errors pra acompanhar o disparo.

O mês é o do calendário, no horário de Brasília.

Limites

  • 600 chamadas por minuto por chave. Os cabeçalhos RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset vêm em toda resposta; acima disso, 429 com Retry-After.
  • 1200 chamadas por minuto por conta, somando todas as chaves. Upload de arquivo (POST /v1/media): 30 por minuto por chave e 1 GB de arquivo novo por dia (o mesmo arquivo enviado de novo não conta).
  • Corpo do pedido: até 256 KB de JSON.
  • Fila de envio: os envios saem em ordem, a até ~50 por segundo. Mensagem que espera mais de 15 minutos na fila não sai mais: vira failed e o seu sistema decide se reenvia.
  • Até 20 chaves ativas e 10 webhooks por conta.
  • Limite da Meta por número: quantas pessoas diferentes você pode chamar por template em 24h é definido pela Meta pra sua conta (aparece em GET /v1/channels, campo messaging_limit) e sobe sozinho com uso e boa qualidade.

Erros

Todo erro tem o mesmo formato:

{ "error": { "code": "invalid_request", "message": "Alguns campos não passaram na validação.",
  "details": [ { "field": "to", "message": "Telefone com DDI, só números (10 a 15 dígitos)" } ],
  "request_id": "req_8d1c..." } }
HTTPcodeO que significa
400invalid_requestO corpo ou os parâmetros não passaram na validação. details diz qual campo.
401unauthorizedChave ausente, inválida ou revogada.
402account_suspendedA conta está suspensa (pagamento pendente). Nada é enviado até regularizar.
403forbiddenA chave não pode fazer esta operação.
404not_foundO recurso não existe nesta conta.
409conflictA operação conflita com o estado atual do recurso.
409idempotency_conflictA mesma Idempotency-Key foi usada com um corpo diferente.
409channel_not_connectedO número não está conectado à API oficial agora.
422channel_ambiguousA conta tem mais de um número: informe channel_id.
422outside_customer_service_windowFora da janela de 24h desde a última mensagem da pessoa, a Meta só aceita template. Envie um template.
422recipient_opted_outA pessoa pediu para não receber marketing. Templates de marketing não são enviados para ela.
429rate_limitedMuitas chamadas em pouco tempo. Respeite o cabeçalho Retry-After.
502meta_errorA Meta recusou a operação. details traz o código e a mensagem dela.
500internal_errorErro do nosso lado. Pode repetir com a mesma Idempotency-Key sem risco de duplicar.

Migrando de outra plataforma?

A troca é direta. O número continua o mesmo (a migração entre parceiros da Meta leva junto nome, qualidade, limites e templates aprovados). A tabela abaixo mostra a equivalência com um formato comum de API v2 de mensageria.

Plataforma anterior (API v2)Unred (API v1)
X-API-TOKEN: …Authorization: Bearer unr_live_…
POST /v2/channels/whatsapp/messagesPOST /v1/messages
from (remetente)channel_id (opcional com um número só)
contents: [{ "type": "text", "text": "…" }]"type": "text", "text": { "body": "…" }
contents: [{ "type": "template", "templateId", "fields" }]"type": "template", "template": { "name", "language", "components" }
contents: [{ "type": "file", "fileUrl", "fileCaption" }]"type": "image" | "document" | …, "<tipo>": { "link", "caption" }
POST /v2/subscriptions (MESSAGE, MESSAGE_STATUS)POST /v1/webhooks (message.received, message.sent/delivered/read/failed)
messageStatus.code: SENT, DELIVERED, READ, REJECTED, NOT_DELIVEREDstatus: sent, delivered, read, failed
/v2/templates/v1/templates
/v2/reports/message/entriesGET /v1/messages e GET /v1/usage

Diferenças que ajudam: chave de teste que não envia nada, Idempotency-Key, mídia recebida com link permanente, reenvio automático de webhook e o objeto original da Meta em toda mensagem recebida.

Implantando com IA

Esta documentação foi escrita pra ser lida por pessoas e por assistentes de código. Dê ao seu assistente:

Peça, por exemplo: "Integre o meu sistema à API da Unred usando o openapi.json: envie o template X quando o pedido for faturado, com Idempotency-Key = id do pedido, e receba os webhooks em /webhooks/unred conferindo a assinatura."