Guia de integração — Orquestra WhatsApp Gateway
Guia para quem vai ligar um sistema (condomínio, oficina, loja, ERP) ao gateway e enviar mensagens de WhatsApp. Descreve a API como está em produção, conferida contra o código em 08/11/2026. Começa por um resumo em 5 passos; a lista de todas as rotas está na seção 16.
Comece em 5 passos
Integrar leva poucos minutos e duas chamadas HTTP. Nada de servidor de WhatsApp para manter: o gateway cuida da conexão, da fila, dos limites e da entrega.
- Receba seus dados de acesso. O gestor do gateway envia o link, a chave de API e o número que vai enviar (botão Copiar dados de integração). Guarde a chave no servidor do seu sistema.
- Conecte o número. Leia o QR Code com o WhatsApp do celular, no painel ou dentro do seu próprio sistema (seção 8).
- Teste a integração. Troque a chave por um token e chame
GET /v1/diagnostico: a resposta diz, em português, se está tudo pronto ou o que falta. - Envie a primeira mensagem.
POST /v1/mensagenscom o telefone (pode ser(27) 99999-9999) e o texto. Pronto. - Acompanhe. Consulte a situação ou receba avisos por webhook. Quando quiser, ligue anexos, enquetes e o recebimento de respostas.
# 1) token (vale 5 minutos)
curl -X POST https://gateway.orquestrasistemas.com.br/v1/autenticacao/token -H "X-API-Key: <SUA_CHAVE>"
# 2) mensagem
curl -X POST https://gateway.orquestrasistemas.com.br/v1/mensagens \
-H "Authorization: Bearer <TOKEN>" -H "Idempotency-Key: aviso-0001" -H "Content-Type: application/json" \
-d '{"destinatario": "(27) 99999-9999", "texto": "Olá! Seu pedido está pronto.", "consentimento": {"evidencia": "Cliente aceitou avisos no cadastro."}}'
O restante deste guia detalha cada passo, os recursos opcionais e os erros.
O caminho completo, do início ao fim
| Etapa | Chamada | O que observar |
|---|---|---|
| 1. Token | POST /v1/autenticacao/token com X-API-Key |
Guarde o token e reaproveite até perto de expirar (seção 2) |
| 2. Conferir a chave | GET /v1/credenciais/contexto |
Tipo da chave, número e permissões (seção 3) |
| 3. Número conectado? | GET /v1/diagnostico (ou GET /v1/instancias) |
pronto_para_enviar: true; se não, conecte pelo QR (seção 8) |
| 4. Autorização da pessoa | POST /v1/contatos/consentimentos ou o objeto consentimento no envio |
Sem consentimento, o envio responde 422 (seção 6) |
| 5. Enviar | POST /v1/mensagens com Idempotency-Key |
202 = aceita na fila, ainda não entregue (seção 4) |
| 6. Acompanhar | Webhook mensagem.situacao (seção 9) ou GET /v1/mensagens/{mensagem_id} |
entregue, lida, falhou (seção 7) |
| 7. Saldo e limites | GET /v1/saldo, GET /v1/limites |
Evite o 402 acompanhando disponiveis (seções 4.5 e 11) |
| 8. Respostas (se contratou) | Webhook mensagem.recebida ou GET /v1/mensagens-recebidas |
Responda com responder_a (seções 8.1 e 8.2) |
| 9. Quem pediu para sair | POST /v1/contatos/descadastros |
Quem responde SAIR no WhatsApp já é descadastrado sozinho (seções 6, 8.1) |
1. O que você precisa receber
O gestor do gateway entrega três informações, de preferência pelo botão Copiar dados de integração da tela de chaves, por um canal seguro:
| Informação | Exemplo | Para que serve |
|---|---|---|
| Link do gateway | https://gateway.orquestrasistemas.com.br |
Base de todas as chamadas. As rotas da API começam em /v1/. |
| Chave de API | oak_… (segredo exibido uma única vez) |
Vai no cabeçalho X-API-Key, só para pedir o token temporário. Guarde-a no servidor do seu sistema, nunca no navegador, no aplicativo ou no Git. |
| Instância | Condomínio Simon — instancia_id (UUID) |
O número de WhatsApp que envia. Já vem amarrado à chave: você não informa o número remetente nas mensagens. Serve para consultar e para o QR Code. |
Também importam:
- Tipo da chave. Individual envia sempre pelo mesmo número. Geral envia por um grupo de números e o gateway escolhe qual usar; um mesmo destinatário tende a receber sempre do mesmo número.
- Permissões da chave. Peça ao gestor as que o seu sistema vai usar:
| Permissão (código) | Nome canônico | Nome no painel | Libera |
|---|---|---|---|
messages.send |
mensagens:enviar |
Enviar mensagens | Enviar (/v1/mensagens, lote), verificar número, saldo e diagnóstico completo |
messages.read |
mensagens:ler |
Consultar mensagens | Situação e lista de mensagens, votos de enquete, fila, números (/v1/instancias, também com instancias:conectar) e grupo |
messages.cancel |
mensagens:cancelar |
Cancelar mensagens | Cancelar mensagem ainda não despachada |
consents.write |
contatos:escrever |
Registrar consentimento e descadastro | Registrar e consultar consentimento, descadastrar e usar o objeto consentimento no envio |
contatos:ler |
contatos:ler |
Consultar consentimento do destinatário | GET /v1/contatos/consentimentos (só consulta) |
instances.connect |
instancias:conectar |
Conectar o número por QR | Conectar, consultar o QR e desconectar o número da chave |
messages.receive |
mensagens:receber |
Ler mensagens recebidas | Ler, baixar mídia e apagar mensagens recebidas (módulo do plano) |
media.write |
arquivos:enviar |
Enviar arquivos | Enviar documentos, imagens, áudio e vídeo |
media.read |
arquivos:ler |
Consultar arquivos | Consultar e baixar os arquivos enviados pela chave |
analytics.read |
consumo:ler |
Consultar consumo | GET /v1/consumo, /v1/saldo, /v1/limites e /v1/assinatura |
campaigns.write |
campanhas:iniciar |
Campanhas | Criar, listar e iniciar campanhas (a aprovação é sempre feita por uma pessoa no painel) |
Os dois nomes de cada linha são equivalentes: uma chave com mensagens:enviar vale onde se pede messages.send e vice-versa, e quem pode registrar consentimento também pode consultá-lo. Se faltar alguma, o gestor abre Chaves de API › Editar permissões no painel e marca o que falta: o segredo não muda, o seu sistema não precisa trocar nada e a permissão vale na hora, inclusive para o token já emitido.
Para descobrir, a qualquer momento, a instância e as permissões da sua chave: GET /v1/credenciais/contexto (seção 3). Para conferir se está tudo pronto para enviar: GET /v1/diagnostico (seção 3).
2. Autenticação: chave → token temporário
A chave nunca acompanha as mensagens. Ela só troca por um token temporário (300 segundos), usado nas demais chamadas.
curl -X POST https://gateway.orquestrasistemas.com.br/v1/autenticacao/token \
-H "X-API-Key: <CHAVE_DE_API>"
{ "token_acesso": "oat_…", "token_tipo": "Bearer", "expira_em_segundos": 300 }
- Use
Authorization: Bearer <token_acesso>nas outras rotas. O corpo do pedido de token é ignorado (pode ir vazio). - Reaproveite o token até uns 30 segundos antes de expirar; não peça um token por mensagem (limite: 30 emissões por minuto). Pedir outro não revoga o anterior.
- Token expirado responde
401 TOKEN_INVALIDO: peça outro e repita a chamada com a mesmaIdempotency-Key. - Conta suspensa, envios pausados, assinatura vencida, aplicação inativa ou chave revogada respondem
403na hora, mesmo com token válido. Mensagens já aceitas continuam na fila e só saem quando a situação for regularizada.
3. Conferir a credencial
curl https://gateway.orquestrasistemas.com.br/v1/credenciais/contexto \
-H "Authorization: Bearer <TOKEN>"
{
"chave_id": "…",
"tipo_destino": "fixo",
"instancia_id": "…",
"grupo_balanceamento_id": null,
"escopos": ["messages.send", "messages.read"]
}
tipo_destino fixo é chave individual (tem instancia_id); balanceado é chave geral (tem grupo_balanceamento_id). Qualquer token válido pode consultar.
Testar a integração (botão "Testar integração")
GET /v1/diagnostico confere, sem enviar mensagem, tudo o que o gateway exige para despachar: chave ativa e com permissão de envio, conta e aplicação ativas, assinatura vigente, saldo de mensagens do plano, número conectado e não pausado, horário de envio, limites por minuto/hora/dia e fila andando. Qualquer token válido pode chamar (até 20 vezes por minuto); chave sem a permissão de envio recebe só a indicação de que falta essa permissão.
curl "https://gateway.orquestrasistemas.com.br/v1/diagnostico?destinatario=(27)%2099999-9999" \
-H "Authorization: Bearer <TOKEN>"
{
"pronto_para_enviar": false,
"resumo": "Não é possível enviar agora: \"Portaria\" não está conectado (situação: disconnected). Leia o QR Code para conectar.",
"verificacoes": [
{
"codigo": "chave",
"titulo": "Chave de API ativa",
"situacao": "ok",
"detalhe": "Sem data de expiração."
},
{
"codigo": "saldo",
"titulo": "Saldo de mensagens do plano",
"situacao": "ok",
"detalhe": "488 de 500 disponíveis até 07/12/2026, 11:00 (12 enviadas, 0 na fila)."
},
{
"codigo": "numero",
"titulo": "Número conectado",
"situacao": "erro",
"detalhe": "\"Portaria\" não está conectado (situação: disconnected). Leia o QR Code para conectar."
}
],
"credencial": {
"chave_id": "…",
"tipo_destino": "fixo",
"instancia_id": "…",
"grupo_balanceamento_id": null,
"escopos": ["…"]
},
"numeros": [
{
"instancia_id": "…",
"nome": "Portaria",
"situacao": "disconnected",
"envios_pausados": false,
"janela_aberta": true
}
],
"plano": {
"modelo_cobranca": "mensal",
"limite_mensagens": 500,
"usadas_no_periodo": 12,
"na_fila": 0,
"disponiveis": 488,
"…": "…"
},
"destino": {
"destinatario": "+5527999999999",
"finalidade": "transacional",
"consentido": true,
"descadastrado": false
},
"verificado_em": "…"
}
pronto_para_enviarétruequando nenhum item está comoerro;avisonão impede o envio (por exemplo, fora do horário: a mensagem espera a janela abrir).resumojá vem pronto para mostrar ao usuário; cada item deverificacoestraztituloedetalheem português. Códigos possíveis:chave,permissao_envio,conta,aplicacao,assinatura,saldo,numero,janela,limites,filaedestinatario.destinatario(opcional, exige permissão de envio) simula a checagem do contato: telefone válido, consentimento para afinalidade(padrãotransacional) e descadastro. Sem consentimento, o item fica comoavisose a chave pode registrá-lo no envio (objetoconsentimento) e comoerrose não pode. Nada é gravado nem enviado.- Um botão "Testar integração" no seu sistema faz: pedir o token (seção 2; erro aqui significa chave inválida ou conta bloqueada) → chamar
GET /v1/diagnostico→ mostrarresumoe a lista. Para a prova final, envie uma mensagem real para o seu próprio número.
No painel, a ação Testar de cada chave mostra o mesmo resultado (proprietário, administrador e auditor).
4. Enviar uma mensagem
curl -X POST https://gateway.orquestrasistemas.com.br/v1/mensagens \
-H "Authorization: Bearer <TOKEN>" \
-H "Idempotency-Key: pedido-1234-pronto" \
-H "Content-Type: application/json" \
-d '{
"destinatario": "(27) 99999-9999",
"texto": "Seu veículo está pronto para retirada.",
"consentimento": { "evidencia": "Cliente aceitou avisos de serviço no cadastro." }
}'
Resposta 202 (aceita na fila, ainda não entregue):
{
"mensagem_id": "…",
"situacao": "na_fila",
"criado_em": "…",
"consulta": "/v1/mensagens/…",
"requisicao_id": "…"
}
| Campo | Obrigatório | Regra |
|---|---|---|
destinatario |
sim | Telefone. Veja a seção 5. |
texto |
um conteúdo | Até 16.000 caracteres, com emojis e a formatação do WhatsApp (*negrito*, _itálico_). |
arquivo_id |
um conteúdo | Arquivo enviado antes por POST /v1/arquivos (seção 4.2), com legenda opcional (até 4.000 caracteres). Com áudio OGG, "voz": true chega como mensagem de voz gravada. |
localizacao |
um conteúdo | { "latitude": -20.31, "longitude": -40.31, "nome": "Oficina", "endereco": "…" } — chega como mapa. |
contato |
um conteúdo | { "nome": "Portaria", "telefone": "(27) 3333-4444", "empresa": "…" } — chega como cartão de contato com botão para conversar. |
enquete |
um conteúdo | { "pergunta": "Aprova a reforma?", "opcoes": ["Sim", "Não"], "multipla": false } — 2 a 12 opções diferentes; resultado em GET /v1/mensagens/{id}/votos (seção 8.3). |
responder_a |
não | mensagem_recebida_id de uma mensagem recebida: a resposta chega citando a original (seção 8.2). |
finalidade |
não | Motivo do contato; padrão transacional. Precisa ser exatamente a do consentimento. Não pode começar com resposta: (reservado). |
consentimento |
não | { "evidencia": "…", "concedido_em": "…" }. Registra a autorização no primeiro envio (seção 6). |
agendar_para |
não | Data e hora ISO 8601 com fuso (ex.: 2026-11-20T09:00:00-03:00), até 30 dias à frente. |
campanha_id |
não | Só para campanhas aprovadas desta chave (seção 10). |
metadados |
não | Até 20 pares texto → texto (até 500 caracteres cada), para o seu controle (por exemplo, número do pedido). Ficam guardados cifrados e não voltam nas consultas. |
Idempotency-Keyé obrigatório. Use um identificador único por mensagem (1 a 120 caracteres: letras, números,_ . : -; por exemplo,pedido-1234-pronto). Se a rede cair e você repetir o mesmo pedido com a mesma chave, o gateway devolve a mesma mensagem (com a situação atual) em vez de enviar duas vezes — mesmo que você tenha trocado de token. A mesma chave com conteúdo diferente responde 409. Sem o cabeçalho, 400.- Não envie instância, número remetente nem organização no corpo: quem decide é a chave. Campos desconhecidos respondem
400 ENTRADA_INVALIDA. - Cada mensagem leva exatamente um conteúdo:
texto,arquivo_id,localizacao,contatoouenquete. Dois conteúdos (ou nenhum) respondem 400. - O
202é imediato. A mensagem sai respeitando o horário de envio, os limites e o espaçamento do número (seção 11).
4.1 Enviar em lote
Até 100 mensagens numa chamada, cada uma com sua própria chave de idempotência. Um item recusado não impede os outros.
curl -X POST https://gateway.orquestrasistemas.com.br/v1/mensagens/lote \
-H "Authorization: Bearer <TOKEN>" -H "Idempotency-Key: lote-2026-11-08-0001" -H "Content-Type: application/json" \
-d '{ "itens": [
{ "chave_idempotencia": "revisao-1001", "mensagem": { "destinatario": "(27) 99999-9999", "texto": "Revisão amanhã às 9h." } },
{ "chave_idempotencia": "revisao-1002", "mensagem": { "destinatario": "(27) 98888-7777", "texto": "Revisão amanhã às 10h." } }
] }'
{
"itens": [
{
"chave_idempotencia": "revisao-1001",
"aceita": true,
"mensagem_id": "…",
"situacao": "na_fila",
"criado_em": "…",
"consulta": "/v1/mensagens/…"
},
{
"chave_idempotencia": "revisao-1002",
"aceita": false,
"erro": {
"codigo": "CONSENTIMENTO_AUSENTE",
"mensagem": "Destinatário sem consentimento válido para esta finalidade. …",
"status": 422
}
}
],
"requisicao_id": "…"
}
- O cabeçalho
Idempotency-Key(8 a 200 caracteres) identifica o lote: repetir o mesmo lote devolve a mesma resposta; outro conteúdo com a mesma chave responde 409. Cadachave_idempotencia(8 a 200 caracteres) identifica a mensagem e precisa ser diferente das outras do lote (senão 422IDEMPOTENCIA_DUPLICADA). - Os itens voltam ordenados por
chave_idempotencia, não na ordem enviada: case pelo campo, não pela posição. - Itens que passarem do saldo do plano são recusados com
LIMITE_DO_PLANO_ATINGIDO; os anteriores continuam aceitos.
4.2 Enviar arquivos (anexos)
São duas chamadas: primeiro o arquivo, depois a mensagem com o arquivo_id.
# 1) Envia o arquivo (multipart, campo "arquivo"). Exige a permissão media.write.
curl -X POST https://gateway.orquestrasistemas.com.br/v1/arquivos \
-H "Authorization: Bearer <TOKEN>" \
-F "arquivo=@orcamento.pdf;type=application/pdf"
# → 202 { "arquivo_id": "…", "situacao": "disponivel", "tipo_mime": "application/pdf", "tamanho_bytes": 48211 }
# 2) Envia a mensagem com o arquivo.
curl -X POST https://gateway.orquestrasistemas.com.br/v1/mensagens \
-H "Authorization: Bearer <TOKEN>" -H "Idempotency-Key: orcamento-1234" -H "Content-Type: application/json" \
-d '{ "destinatario": "(27) 99999-9999", "arquivo_id": "…", "legenda": "Seu orçamento" }'
| Formato | Chega como |
|---|---|
| PDF, Word (DOCX), Excel (XLSX), PowerPoint (PPTX) | Documento, com o nome original do arquivo |
| JPEG, PNG, WebP | Foto, com a legenda |
| MP3, M4A, OGG | Áudio; OGG com "voz": true chega como mensagem de voz gravada |
| MP4 | Vídeo, com a legenda |
- Até 16 MB por arquivo, um arquivo por envio e por mensagem. O formato é conferido pelo conteúdo (não pelo nome) e precisa bater com o tipo informado no envio; arquivos do Office com macros são recusados (
415 TIPO_INVALIDO). - Todo arquivo passa por antivírus durante a chamada: quando ela responde, o arquivo já pode ser usado. Se o antivírus não responder,
503 ANTIVIRUS_INDISPONIVEL— tente de novo em instantes. Um envio de arquivo por vez por organização (429 UPLOAD_OCUPADO). - O arquivo fica guardado cifrado, visível só para a sua chave (outra chave da mesma organização não consegue usá-lo). O espaço conta no limite de armazenamento do plano (
413 ARMAZENAMENTO_ESGOTADO). - O mesmo
arquivo_idpode ser usado em várias mensagens (por exemplo, um comunicado em PDF para vários moradores). - Consultar:
GET /v1/arquivos/{arquivo_id}(nome, tipo, tamanho, situação) eGET /v1/arquivos/{arquivo_id}/conteudo(baixa o arquivo), com a permissãomedia.read.
4.3 Verificar se o número tem WhatsApp
curl -X POST https://gateway.orquestrasistemas.com.br/v1/contatos/verificar \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{ "destinatario": "(27) 99999-9999" }'
# → 200 { "destinatario": "+5527999999999", "tem_whatsapp": true, "verificado_em": "…", "origem": "consulta" }
- Consulta o WhatsApp sem enviar mensagem, pelo número da sua chave (exige
messages.sende o número conectado; senão409 INSTANCIA_DESCONECTADA). Leva até ~12 segundos; se o WhatsApp não responder,503 VERIFICACAO_INDISPONIVEL— tente de novo. - O resultado vale por 7 dias (
origem: "recente"). Limite de 500 consultas novas por dia por organização e 30 por minuto: consultas em massa parecem varredura e podem levar ao bloqueio do número. - No painel, cada número pode ligar "Conferir se o destinatário tem WhatsApp antes de cada envio": mensagens para números sem WhatsApp terminam como
falhou(motivonumero_sem_whatsapp) sem tentar enviar, e o gateway usa o identificador real do contato — o que evita falhas com números antigos registrados sem o nono dígito.
4.4 Agendar e cancelar
- Agendar:
"agendar_para": "2026-11-20T09:00:00-03:00"(até 30 dias à frente; mais que isso,422 AGENDAMENTO_DISTANTE). A mensagem aparece comona_filae emGET /v1/filascomagendado_para. Ela também respeita o horário de envio do número. - Prazo: uma mensagem que não conseguir sair até 24 horas depois do horário previsto (número desconectado, limites, fora do horário) passa a
expiradae não é mais enviada. - Cancelar:
POST /v1/mensagens/{mensagem_id}/cancelar(permissãomessages.cancel) enquanto estiverna_filaouagendada→200 {"cancelada": true}. Depois que o envio começou, ou se já terminou,409 CANCELAMENTO_INDISPONIVEL.
curl -X POST https://gateway.orquestrasistemas.com.br/v1/mensagens/<MENSAGEM_ID>/cancelar \
-H "Authorization: Bearer <TOKEN>"
Rotas
POSTsem corpo (cancelar, conectar, desconectar, iniciar campanha): envie semContent-Type, ou comContent-Type: application/jsone o corpo{}. Cabeçalho JSON com corpo vazio responde400.
4.5 Saldo do plano
Cada plano tem um limite de mensagens por período da assinatura (do início ao fim da assinatura, não do mês do calendário), somando todas as chaves e números da organização. Consulte com GET /v1/saldo (permissão messages.send ou analytics.read):
{
"modelo_cobranca": "mensal",
"limite_mensagens": 500,
"limite_efetivo": 500,
"teto_excedente_mensagens": 0,
"excedente_usado": 0,
"valor_mensagem_excedente": 0,
"valor_excedente_estimado_centavos": 0,
"usadas_no_periodo": 402,
"na_fila": 15,
"disponiveis": 83,
"percentual_usado": 83,
"periodo_inicio": "2026-11-07T15:56:04.885Z",
"periodo_fim": "2026-12-07T15:56:04.885Z",
"recebimento": {
"incluido": true,
"limite_recebidas": 100,
"guardadas_no_periodo": 12,
"excedentes": 0,
"descartadas": 0,
"valor_excedente_centavos": 0,
"a_cobrar_centavos": 0,
"retencao_maxima_dias": 30
}
}
modelo_cobranca diz como o plano cobra:
| Modelo | Como funciona | Campos que importam |
|---|---|---|
mensal |
Mensalidade com limite de mensagens no período. Ao atingir o limite, o envio para até a renovação. | limite_mensagens, disponiveis, periodo_fim (renovação) |
franquia_excedente |
A mensalidade inclui limite_mensagens; o que passar, até teto_excedente_mensagens, é cobrado na próxima fatura a valor_mensagem_excedente por mensagem. excedente_usado e valor_excedente_estimado_centavos mostram o acumulado. |
limite_efetivo (franquia + teto), excedente_usado, valor_excedente_estimado_centavos |
pacote_creditos |
Créditos pré-pagos (limite_mensagens) válidos até periodo_fim; comprar outro pacote soma os créditos que sobraram. |
disponiveis (créditos restantes), periodo_fim (validade) |
Exemplo de franquia com excedente: franquia de 1.000, teto de 200 excedentes a R$ 0,12 cada; já foram 1.035 mensagens.
{
"modelo_cobranca": "franquia_excedente",
"limite_mensagens": 1000,
"teto_excedente_mensagens": 200,
"limite_efetivo": 1200,
"usadas_no_periodo": 1035,
"excedente_usado": 35,
"valor_mensagem_excedente": 0.12,
"valor_excedente_estimado_centavos": 420,
"na_fila": 12,
"disponiveis": 153,
"percentual_usado": 87,
"…": "…"
}
O envio é bloqueado ao atingir limite_efetivo (limite do plano + teto de excedente, na franquia).
- Mensagens na fila já reservam saldo; cada mensagem conta uma vez, mesmo com reenvio, e as que falharam não contam.
- Com
disponiveisem zero, o envio responde 402LIMITE_DO_PLANO_ATINGIDOcom a data de renovação notitle. Não repita automaticamente: avise o responsável, aguarde a renovação ou contrate um plano maior. - No lote, só os itens que passarem do saldo são recusados. Uma campanha só começa se o saldo cobrir todos os destinatários.
- A repetição de uma mensagem já aceita (mesma
Idempotency-Key) continua respondendo 202, sem consumir saldo de novo. recebimentomostra o módulo de mensagens recebidas (seção 8.1);incluido: falsequando o plano não tem o módulo.GET /v1/assinatura(permissãoanalytics.read) devolve a vigência (inicio_em,fim_em) e os limites contratados.
5. Formato do telefone
Envie o número como o seu sistema tiver guardado. O gateway remove pontuação, espaços, traços e parênteses e inclui o +55 quando houver DDD + número:
| Você envia | O gateway usa |
|---|---|
(27) 99999-9999, 27 99999.9999, 27999999999 |
+5527999999999 |
027 99999-9999, 55 27 99999-9999, 0055… |
+5527999999999 |
(27) 3333-4444 (fixo) |
+552733334444 |
+351 912 345 678 (outro país, com +) |
+351912345678 |
Número sem DDD ou estrangeiro sem + responde 400 TELEFONE_INVALIDO: o gateway não adivinha país. As consultas devolvem o número no formato internacional (+55…).
Isso vale para os campos destinatario e destinatarios no corpo das requisições, para o telefone do cartão de contato, e para os filtros remetente (mensagens recebidas) e destinatario (diagnóstico). Exceção: a consulta GET /v1/contatos/consentimentos exige o telefone já em E.164, com o + codificado como %2B (seção 6).
6. Consentimento
Toda mensagem precisa de autorização do destinatário para a finalidade (LGPD e regras do WhatsApp). Duas formas:
- Antes do envio:
POST /v1/contatos/consentimentoscomdestinatario,finalidade,evidenciaeconcedido_emopcional (por exemplo, no cadastro do cliente). Exigeconsents.write.
bash
curl -X POST https://gateway.orquestrasistemas.com.br/v1/contatos/consentimentos \
-H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" \
-d '{ "destinatario": "(27) 99999-9999", "finalidade": "transacional",
"evidencia": "Aceitou avisos por WhatsApp no formulário de cadastro em 01/11/2026." }'
# → 201 { "id": "…" }
- No primeiro envio: o objeto
consentimentona mensagem (exige a permissãoconsents.write). Nos envios seguintes ele já existe e pode ser omitido (se vier, é ignorado).
Consultar antes de registrar ou enviar: GET /v1/contatos/consentimentos?destinatario=%2B5527999999999&finalidade=transacional (permissão contatos:ler ou contatos:escrever) responde sempre 200:
{
"destinatario": "+5527999999999",
"finalidade": "transacional",
"consentido": true,
"descadastrado": false,
"registro_id": "…",
"concedido_em": "2026-11-07T10:00:00Z",
"expira_em": null
}
O telefone vai em E.164 (+55…, com o + codificado como %2B) e a finalidade precisa ser exatamente a usada no registro. Descadastro prevalece (consentido=false, descadastrado=true). A consulta não cria nada: não registre consentimento só porque ela voltou falso; registre quando houver a autorização da pessoa.
Regras:
- A
evidenciadescreve como a pessoa autorizou (10 a 2.000 caracteres, sem dados sensíveis).concedido_emno futuro responde422 DATA_INVALIDA. - A finalidade é livre (até 120 caracteres), mas precisa ser a mesma no consentimento e na mensagem: um consentimento
transacionalnão vale paramarketing. - Quem pedir para sair:
POST /v1/contatos/descadastroscomdestinatario(emotivoopcional) →200 {"descadastrado": true}. Vale para todas as finalidades e na hora: novos envios respondem422 CONSENTIMENTO_AUSENTEe mensagens que estavam na fila são canceladas antes de sair. Novo consentimento para quem saiu responde409 DESTINATARIO_DESCADASTRADO(exige revisão administrativa). - Quem responder
SAIR,PARAR,CANCELAR,DESCADASTRAR,REMOVERouSTOPno WhatsApp é descadastrado automaticamente (seção 8.1). - Campanhas exigem consentimento registrado antes (o objeto
consentimentonão vale comcampanha_id).
7. Acompanhar a entrega
GET /v1/mensagens/{mensagem_id} devolve situacao, motivo_fila e datas. GET /v1/mensagens lista as 100 mais recentes da sua chave (sem filtros nesta versão). Ambos exigem messages.read.
{
"mensagem_id": "…",
"situacao": "entregue",
"motivo_fila": null,
"criado_em": "…",
"atualizado_em": "…"
}
| Situação | Significado |
|---|---|
na_fila |
Aceita, aguardando vez (limites, horário, conexão ou horário agendado) |
reservada |
Separada para envio |
em_envio |
Sendo enviada |
aceita_provedor |
O WhatsApp recebeu |
entregue |
Chegou ao celular |
lida |
Foi lida (se o destinatário permite confirmação de leitura) |
falhou |
Não foi possível enviar |
incerta |
Sem confirmação; não é reenviada automaticamente (evita mensagem duplicada) |
expirada |
Passou 24 h do horário previsto sem conseguir enviar |
cancelada |
Cancelada por você, por descadastro/consentimento revogado ou por encerramento da conta |
motivo_fila explica a espera (ou o fim) da mensagem:
motivo_fila |
Significado |
|---|---|
aguardando_intervalo_entre_envios |
Espaçamento entre envios do número (seção 11); sai em segundos |
sem_instancia_elegivel_quota_janela_ou_afinidade |
Número desconectado, fora do horário de envio ou no limite por minuto/hora/dia |
quota_assinatura |
Saldo do plano esgotado |
conta_assinatura_ou_chave_indisponivel |
Conta, assinatura ou chave indisponível (a fila fica pausada) |
grupo_pausado_ou_restrito, grupo_com_restricao_requer_revisao, grupo_sem_minimo_de_membros |
Problema no grupo da chave geral: fale com o gestor |
campanha_pausada, campanha_aguarda_provedor_oficial |
Mensagem de campanha (seção 10) |
consentimento_ausente_ou_revogado |
Cancelada: a pessoa saiu ou o consentimento foi revogado |
numero_sem_whatsapp |
Falhou: o número não tem WhatsApp (conferência prévia ligada) |
reconciliacao_necessaria |
Incerta: o serviço reiniciou durante o envio |
Também ajudam: GET /v1/filas (até 100 mensagens da chave em na_fila, agendada ou incerta, com o motivo) e GET /v1/consumo (total de mensagens da chave no mês, por situação).
Para não ficar consultando, use webhooks (seção 9).
8. Conectar o número pelo seu sistema (QR Code)
Para o administrador do cliente ler o QR Code dentro do seu sistema, sem acesso ao painel do gateway. Exige a permissão instancias:conectar (ou instances.connect) e, para acompanhar a situação, messages.read. Chave individual acessa só o próprio número; chave geral, só os números ativos do seu grupo.
- Descubra o número:
GET /v1/credenciais/contexto(instancia_id) ouGET /v1/instancias. POST /v1/instancias/{instancia_id}/conectar(sem corpo, ou{}) →200com{"solicitada", "instancia_id", "situacao"}. Se já estiver conectado, ou já houver conexão/QR em andamento,solicitadavemfalsee nada é reiniciado: não chameconectara cada atualização da tela.- A cada 3 segundos,
GET /v1/instancias/{instancia_id}/qr: 200comqreexpira_em: desenhe o conteúdo deqrcomo QR Code no seu servidor (ele troca sozinho a cada 20 a 60 segundos);404 QR_INDISPONIVEL: ainda gerando, já lido ou conectado. Confira asituacaoemGET /v1/instancias(ou no diagnóstico):connectedsignifica pareado; feche a tela.- Para trocar de número:
POST /v1/instancias/{instancia_id}/desconectare repita. Desconectar só funciona com chave individual (chave geral:409 CHAVE_SEM_INSTANCIA); limite de 10 por minuto.
A chave e o token ficam no seu servidor; o navegador recebe só a imagem do QR. Não guarde o QR em logs nem em URLs. Instância restrita pelo WhatsApp responde 409 INSTANCIA_RESTRITA e exige análise do gestor. Número de outra chave responde 404 INSTANCIA_INDISPONIVEL. Cada pedido de conexão, desconexão e cada QR exibido ficam na auditoria (sem o conteúdo do QR).
Situações do número (situacao em GET /v1/instancias), com os nomes que a API devolve hoje: created (nunca conectado), qr_pending (aguardando leitura do QR), connecting, connected (único que envia), reconnecting, disconnected, logged_out (sessão encerrada no celular: leia o QR de novo), restricted (restringido pelo WhatsApp), paused e error.
8.1 Receber mensagens (módulo do plano)
Cada número tem um modo de recebimento, escolhido pelo gestor no painel (Instâncias › Editar):
| Modo | O que acontece |
|---|---|
| Ignorar | Nada é lido. |
| Só descadastro automático (padrão) | Quem responder só SAIR, PARAR, CANCELAR, DESCADASTRAR, REMOVER ou STOP é descadastrado na hora (envios para ele passam a ser recusados). Nenhuma mensagem é guardada. |
| Recebimento completo | Além do descadastro, cada mensagem é guardada cifrada pelo prazo escolhido (até o máximo do plano), avisada por webhook e consultável pela API. Exige o módulo no plano. |
Grupos e o histórico antigo do celular são ignorados (grupos podem ser ligados por número). O módulo é cobrado pelo plano: uma cota de mensagens guardadas por período e, se o plano prever, um valor por mensagem acima da cota; sem esse valor, acima da cota as mensagens deixam de ser guardadas. O consumo aparece em GET /v1/saldo (campo recebimento).
Com a permissão messages.receive:
# Novas desde a última consulta (ordem de chegada; guarde o recebido_em da última):
curl "https://gateway.orquestrasistemas.com.br/v1/mensagens-recebidas?apos=2026-11-07T18:00:00Z" \
-H "Authorization: Bearer <TOKEN>"
{
"itens": [
{
"mensagem_recebida_id": "…",
"instancia_id": "…",
"remetente": "+5527999999999",
"nome_exibicao": "Maria",
"tipo": "texto",
"texto": "Meu carro está pronto?",
"legenda": null,
"nome_arquivo": null,
"voz": false,
"localizacao": null,
"contato": null,
"reacao": null,
"tem_midia": false,
"midia_tipo": null,
"midia_tamanho": null,
"resposta_a_mensagem_id": "…",
"recebido_em": "…",
"expira_em": "…"
}
]
}
- Filtros:
apos(data/hora),remetente(telefone em qualquer formato),instancia_id,limite(1 a 100; padrão 50). Semapos, vêm as mais recentes primeiro. tipo:texto,imagem,audio,video,documento,figurinha,localizacao,contato,reacaoououtro; os camposlegenda,nome_arquivo,voz,localizacao,contatoereacaovêm conforme o tipo.resposta_a_mensagem_idindica a mensagem nossa que a pessoa respondeu (citou).- Mídia:
GET /v1/mensagens-recebidas/{id}/midiadevolve o arquivo (até 16 MB; maiores ficam só com os dados da mensagem). Sem mídia guardada,404 MIDIA_INDISPONIVEL. GET /v1/mensagens-recebidas/{id}traz uma;DELETE /v1/mensagens-recebidas/{id}apaga antes do prazo (pedido do titular, LGPD) e responde204.- O número do remetente pode vir
nullquando o WhatsApp o oculta (contas com identificador privado).
8.2 Responder (atendimento, "responda 1 para confirmar")
Envie a mensagem normalmente com responder_a = mensagem_recebida_id. Ela sai pelo mesmo número que recebeu e chega citando a original:
{ "destinatario": "+5527999999999", "texto": "Está pronto, pode retirar.", "responder_a": "…" }
Quem escreveu nas últimas 24 horas autoriza a resposta: o gateway registra esse consentimento sozinho, com a mensagem recebida como evidência, válido por 24 horas e preso àquela conversa (finalidade resposta:<id>). Ele vale só para atendimento (finalidade transacional ou atendimento), não autoriza outros envios para a pessoa e não vale em campanhas (RESPOSTA_EM_CAMPANHA). Depois disso, ou para quem pediu para sair, valem as regras normais da seção 6. O destinatario precisa ser quem escreveu (422 RESPOSTA_OUTRO_DESTINATARIO). Para um robô de confirmação, leia o texto recebido (por exemplo, 1) e responda com responder_a.
8.3 Enquetes
{
"destinatario": "+5527999999999",
"enquete": { "pergunta": "Aprova a reforma da fachada?", "opcoes": ["Sim", "Não", "Abstenção"] }
}
- 2 a 12 opções diferentes (até 100 caracteres cada; pergunta até 255);
"multipla": truepermite marcar várias. - Os votos chegam cifrados e o gateway decifra com o segredo gerado no envio; funcionam em qualquer modo de recebimento.
GET /v1/mensagens/{mensagem_id}/votos(permissãomessages.read): pergunta, contagem por opção e o voto mais recente de cada pessoa (opcoesvazio = voto retirado). Mensagem que não é enquete:422 NAO_E_ENQUETE. Webhook:enquete.voto.
{
"mensagem_id": "…",
"pergunta": "Aprova a reforma da fachada?",
"multipla": false,
"total_eleitores": 2,
"opcoes": [
{ "opcao": "Sim", "votos": 1 },
{ "opcao": "Não", "votos": 1 },
{ "opcao": "Abstenção", "votos": 0 }
],
"votos": [{ "eleitor": "+5527999999999", "opcoes": ["Sim"], "votado_em": "…" }]
}
- No painel, a ação Votos aparece nas enquetes da tela Mensagens.
9. Webhooks (avisos automáticos)
Em vez de ficar consultando, cadastre no painel (Webhooks, proprietário ou administrador) uma URL do seu sistema e os eventos desejados. O segredo do webhook (owh_…) aparece uma única vez na criação: guarde-o no servidor.
| Evento | Quando | Corpo (JSON) |
|---|---|---|
mensagem.situacao |
A cada mudança de situação de uma mensagem enviada | evento_id, evento, mensagem_id, situacao, ocorrido_em |
mensagem.recebida |
Mensagem recebida guardada (recebimento completo) | evento_id, evento, mensagem_recebida_id, instancia_id, remetente, tipo, texto (ou legenda), tem_midia, resposta_a_mensagem_id, descadastro, recebido_em |
contato.descadastrado |
Alguém respondeu SAIR (ou equivalente) no WhatsApp |
evento_id, evento, destinatario, instancia_id, motivo (pedido_pelo_whatsapp), ocorrido_em |
enquete.voto |
Voto (ou voto retirado) numa enquete enviada | evento_id, evento, mensagem_id, eleitor, opcoes, votado_em |
Exemplo de mensagem.situacao:
{
"evento_id": "…",
"evento": "mensagem.situacao",
"mensagem_id": "…",
"situacao": "entregue",
"ocorrido_em": "2026-11-08T15:57:02.114Z"
}
mensagem.situacao é enviado para na_fila (no aceite), em_envio, aceita_provedor, entregue, lida, falhou e incerta. Cancelamentos e expirações não geram webhook nesta versão: consulte GET /v1/mensagens/{mensagem_id} se precisar confirmá-los. Só entram eventos ocorridos depois de o webhook ser criado.
Como o gateway chama a sua URL: POST com Content-Type: application/json e os cabeçalhos
| Cabeçalho | Conteúdo |
|---|---|
X-Orquestra-Evento |
O mesmo evento_id do corpo |
X-Orquestra-Timestamp |
Momento do envio, em segundos desde 1970 (UTC) |
X-Orquestra-Assinatura |
sha256=<HMAC-SHA256 em hexadecimal de "<timestamp>.<corpo bruto>"> |
O que o seu sistema deve fazer:
- Ler o corpo bruto (antes de converter o JSON) e calcular
HMAC-SHA256(segredo, timestamp + "." + corpo); comparar com a assinatura em tempo constante. Diferente: responda 401 e ignore. - Recusar timestamp com mais de 5 minutos de diferença do seu relógio (proteção contra repetição).
- Tratar
evento_idjá processado como duplicado (a entrega é "ao menos uma vez"). - Responder
2xxrápido (em até 10 segundos) e processar depois, se for demorado.
Retentativas: erro 5xx, 429, tempo esgotado ou falha de conexão são repetidos com espera crescente (10 s, 20 s, 40 s… até 1 hora, com pequena variação), até 8 tentativas no total. Outro 4xx encerra as tentativas daquele evento. Redirecionamentos não são seguidos.
Requisitos da URL: https://, porta 443, com nome de domínio (não IP) que aponte só para endereços públicos; sem usuário/senha nem # na URL.
// Node.js (Express): conferir a assinatura com o corpo bruto.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const vistos = new Set(); // em produção, guarde no banco
app.post('/webhooks/orquestra', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Orquestra-Timestamp') ?? '';
const recebida = req.get('X-Orquestra-Assinatura') ?? '';
const esperada =
'sha256=' +
crypto
.createHmac('sha256', process.env.ORQUESTRA_SEGREDO_WEBHOOK)
.update(`${timestamp}.${req.body.toString('utf8')}`)
.digest('hex');
const valida =
recebida.length === esperada.length &&
crypto.timingSafeEqual(Buffer.from(recebida), Buffer.from(esperada));
if (!valida || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString('utf8'));
if (!vistos.has(evento.evento_id)) {
vistos.add(evento.evento_id);
// processar: evento.evento, evento.mensagem_id, evento.situacao…
}
res.sendStatus(204);
});
<?php
// PHP: conferir a assinatura com o corpo bruto.
$corpo = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ORQUESTRA_TIMESTAMP'] ?? '';
$esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $corpo, getenv('ORQUESTRA_SEGREDO_WEBHOOK'));
if (!hash_equals($esperada, $_SERVER['HTTP_X_ORQUESTRA_ASSINATURA'] ?? '') || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit;
}
$evento = json_decode($corpo, true);
// ignore $evento['evento_id'] já processado; depois trate $evento['evento']
http_response_code(204);
# Python (Flask): conferir a assinatura com o corpo bruto.
import hashlib, hmac, os, time
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/orquestra")
def receber():
corpo = request.get_data() # bytes, sem converter
timestamp = request.headers.get("X-Orquestra-Timestamp", "")
esperada = "sha256=" + hmac.new(
os.environ["ORQUESTRA_SEGREDO_WEBHOOK"].encode(), timestamp.encode() + b"." + corpo, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(esperada, request.headers.get("X-Orquestra-Assinatura", "")):
return "", 401
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return "", 401
evento = request.get_json()
# ignore evento["evento_id"] já processado; depois trate evento["evento"]
return "", 204
10. Campanhas
Para avisos a muitas pessoas de uma vez (até 500 destinatários por campanha), com a permissão campaigns.write:
- Criar:
POST /v1/campanhascomnome,finalidade,texto,destinatarios(lista de telefones) eagendar_paraopcional →201 {"campanha_id", "situacao": "rascunho", "instrucao"}. - Aprovar: sempre por uma pessoa no painel (proprietário ou administrador). A API não aprova.
- Iniciar:
POST /v1/campanhas/{campanha_id}/iniciar(sem corpo) →202 {"iniciada": true, "mensagens": 120}. É tudo ou nada: só começa se a campanha estiveraprovada(senão409 CAMPANHA_BLOQUEADA), se o saldo cobrir todos (402) e se todos tiverem consentimento registrado para a finalidade (senão422 CONSENTIMENTO_AUSENTEe nada é enfileirado). - Acompanhar:
GET /v1/campanhas(as 100 mais recentes da chave) e as mensagens emGET /v1/filas.
Atenção: com o conector atual do WhatsApp (não oficial), as mensagens de campanha ficam na fila com
motivo_fila=campanha_aguarda_provedor_oficiale expiram em 24 horas: o despacho de campanhas aguarda a integração com o provedor oficial. Para avisos a vários contatos hoje, use o lote (seção 4.1), que segue as regras de consentimento e os limites do número.
Situações da campanha: rascunho, aprovada, agendada, executando, pausada, concluida, cancelada.
11. Limites e espaçamento
| Limite | Valor | O que acontece |
|---|---|---|
| Requisições por IP (todas as rotas) | 120 por minuto | 429 LIMITE_REQUISICOES com Retry-After |
| Emitir token | 30 por minuto | 429 |
| Verificar número | 30 por minuto e 500 consultas novas por dia (organização) | 429 / 429 VERIFICACOES_EXCEDIDAS |
| Diagnóstico | 20 por minuto | 429 |
| Desconectar número | 10 por minuto | 429 |
| Corpo JSON | 128 KiB | 413 |
| Arquivo | 16 MB; um envio por vez por organização | 413 ARQUIVO_GRANDE / 429 UPLOAD_OCUPADO |
| Lote | 100 mensagens | 400 |
| Campanha | 500 destinatários | 400 / 422 DESTINATARIOS_INVALIDOS |
| Mensagens pendentes na organização | 10.000 | 429 FILA_CHEIA |
| Agendamento | Até 30 dias à frente | 422 AGENDAMENTO_DISTANTE |
| Prazo de envio | 24 h depois do horário previsto | Situação expirada |
| Envios por número | Padrão 5 por minuto, 100 por hora, 500 por dia (o gestor ajusta no painel) | Espera na fila (motivo_fila), não falha |
| Horário de envio por número | Padrão 08:00 às 20:00, fuso America/Sao_Paulo (ajustável) |
Espera a janela abrir |
| Saldo do plano | Conforme o plano (seção 4.5) | 402 LIMITE_DO_PLANO_ATINGIDO |
Os cabeçalhos X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset mostram quanto resta da janela de 1 minuto. Os limites do seu número aparecem em GET /v1/limites (permissão analytics.read).
Espaçamento: os envios de cada número saem espaçados por igual — o intervalo é 60 segundos divididos pelo limite por minuto. Com o padrão de 5 por minuto, uma mensagem a cada 12 segundos; com 2, a cada 30; com 3, a cada 20. Mensagens que chegam juntas aguardam a vez (motivo_fila = aguardando_intervalo_entre_envios); o 202 continua imediato. Isso protege o número contra bloqueio por envio em rajada. Na chave geral, cada número do grupo tem seu próprio espaçamento, o que aumenta a vazão total.
12. Erros
Os erros vêm com Content-Type: application/problem+json, no formato:
{
"type": "about:blank",
"codigo": "CONSENTIMENTO_AUSENTE",
"title": "Destinatário sem consentimento válido para esta finalidade. Registre-o antes ou envie o objeto \"consentimento\" junto com a mensagem.",
"status": 422,
"requisicao_id": "req-1a"
}
Use codigo para tratar o erro no seu sistema e title para mostrar ao usuário. Guarde o requisicao_id: ele identifica a chamada para o suporte.
| HTTP | codigo |
Causa | Como resolver |
|---|---|---|---|
| 400 | ENTRADA_INVALIDA |
Campo faltando, a mais ou fora do formato; Idempotency-Key ausente |
Conferir o corpo com a seção 4 (ou o docs/openapi.json) |
| 400 | ENTRADA_INVALIDA |
JSON malformado, ou Content-Type: application/json com corpo vazio (a mensagem explica) |
Enviar JSON válido; em POST sem corpo, não mande Content-Type (ou mande {}) |
| 404 | ROTA_INEXISTENTE |
Caminho ou método que não existe | Conferir a URL e o método na tabela de rotas |
| 400 | TELEFONE_INVALIDO |
Telefone sem DDD ou estrangeiro sem + |
Incluir DDD ou o +código do país (seção 5) |
| 400 | FINALIDADE_RESERVADA, OPCOES_REPETIDAS |
Finalidade resposta:…; opções de enquete repetidas |
Usar outra finalidade; opções diferentes |
| 401 | CHAVE_INVALIDA |
Chave errada ou ausente no pedido de token | Conferir a chave (X-API-Key) |
| 401 | TOKEN_INVALIDO |
Token ausente, expirado ou revogado | Pedir um token novo e repetir com a mesma Idempotency-Key |
| 402 | LIMITE_DO_PLANO_ATINGIDO |
Saldo do plano esgotado | Aguardar a renovação ou ampliar o plano; não repetir automaticamente |
| 403 | ESCOPO_NEGADO |
Falta permissão na chave | Pedir ao gestor a permissão (seção 1) |
| 403 | CONTA_BLOQUEADA |
Conta suspensa, envios pausados, aplicação inativa ou assinatura vencida | Falar com o gestor; as mensagens já aceitas esperam na fila |
| 403 | CHAVE_BLOQUEADA |
Chave revogada, suspensa ou expirada | Pedir uma chave nova |
| 403 | IP_NEGADO |
IP do servidor fora da lista da chave | Pedir ao gestor para incluir o IP |
| 404 | MENSAGEM_INDISPONIVEL, ARQUIVO_INDISPONIVEL, INSTANCIA_INDISPONIVEL, MIDIA_INDISPONIVEL |
Não existe ou é de outra chave (mesma resposta) | Conferir o identificador |
| 404 | QR_INDISPONIVEL |
Sem QR no momento | Continuar consultando; ver a situação em GET /v1/instancias |
| 406 | ERRO_INTERNO (status 406) |
POST /v1/arquivos sem multipart |
Enviar multipart/form-data com o campo arquivo |
| 409 | IDEMPOTENCIA_CONFLITANTE |
Mesma Idempotency-Key com outro conteúdo |
Gerar outra chave para a mensagem nova |
| 409 | DESTINATARIO_DESCADASTRADO |
A pessoa pediu para não receber | Não enviar; novo opt-in exige revisão administrativa |
| 409 | CANCELAMENTO_INDISPONIVEL |
Envio já começou ou terminou | Nada a fazer |
| 409 | INSTANCIA_DESCONECTADA |
Nenhum número conectado para verificar | Conectar o número (seção 8) |
| 409 | INSTANCIA_RESTRITA, CHAVE_SEM_INSTANCIA |
Número restringido pelo WhatsApp; desconectar com chave geral | Falar com o gestor |
| 409 | CAMPANHA_BLOQUEADA |
Campanha não aprovada, inexistente ou de outra chave | Aprovar no painel |
| 413 | ARQUIVO_GRANDE, ARMAZENAMENTO_ESGOTADO |
Arquivo acima de 16 MB; espaço do plano esgotado | Reduzir o arquivo; ampliar o plano |
| 415 | TIPO_INVALIDO |
Formato não aceito ou diferente do declarado | Usar um formato da seção 4.2 com o tipo correto |
| 422 | CONSENTIMENTO_AUSENTE |
Sem consentimento para a finalidade (ou descadastrado) | Registrar o consentimento (seção 6) |
| 422 | ARQUIVO_INDISPONIVEL |
arquivo_id inexistente, de outra chave ou expirado |
Enviar o arquivo de novo com a mesma chave |
| 422 | AGENDAMENTO_DISTANTE |
agendar_para mais de 30 dias à frente |
Agendar mais perto |
| 422 | VOZ_EXIGE_OGG, VOZ_EXIGE_ARQUIVO |
voz sem áudio OGG |
Converter para OGG (Opus) ou tirar voz |
| 422 | RESPOSTA_INDISPONIVEL, RESPOSTA_OUTRO_DESTINATARIO, RESPOSTA_EM_CAMPANHA |
responder_a inválido para este envio |
Conferir o mensagem_recebida_id e o destinatário |
| 422 | NAO_E_ENQUETE |
Votos pedidos de mensagem que não é enquete | Usar o mensagem_id da enquete |
| 422 | IDENTIFICADOR_INVALIDO |
Identificador na URL que não é UUID | Conferir o identificador |
| 422 | IDEMPOTENCIA_DUPLICADA |
Duas mensagens do lote com a mesma chave_idempotencia |
Usar chaves diferentes |
| 422 | DATA_INVALIDA |
concedido_em no futuro |
Usar a data real |
| 422 | ARQUIVO_RECUSADO, ARQUIVO_AUSENTE |
Antivírus recusou; nenhum arquivo enviado | Conferir o arquivo |
| 422 | MODULO_NAO_CONTRATADO |
(Painel) recebimento completo fora do plano | Contratar plano com o módulo |
| 429 | LIMITE_REQUISICOES |
Muitas chamadas por minuto | Esperar o Retry-After e ir mais devagar |
| 429 | FILA_CHEIA, UPLOAD_OCUPADO, VERIFICACOES_EXCEDIDAS |
Fila de 10.000, upload em andamento, teto de verificações | Aguardar e tentar de novo |
| 500 | ERRO_INTERNO |
Falha inesperada | Tentar de novo depois; informar o requisicao_id |
| 503 | ANTIVIRUS_INDISPONIVEL, VERIFICACAO_INDISPONIVEL |
Serviço auxiliar sem resposta | Tentar de novo em instantes |
Quando repetir automaticamente: só em falha de rede, 429, 500 e 503, com espera crescente e a mesma Idempotency-Key. Nunca repita automaticamente 400, 402, 403, 409 ou 422: o resultado será o mesmo até alguém corrigir a causa. Rota inexistente responde 404 sem codigo (formato padrão do servidor).
13. Exemplos completos
Os exemplos leem a chave e o endereço de variáveis de ambiente (ORQUESTRA_CHAVE_API e ORQUESTRA_API_URL), nunca do código. Versões em 8 linguagens: pasta exemplos/ do repositório.
cURL (fluxo completo)
BASE="https://gateway.orquestrasistemas.com.br"
TOKEN=$(curl -s -X POST "$BASE/v1/autenticacao/token" -H "X-API-Key: $ORQUESTRA_CHAVE_API" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["token_acesso"])')
curl -s "$BASE/v1/diagnostico" -H "Authorization: Bearer $TOKEN" # tudo pronto?
curl -s "$BASE/v1/saldo" -H "Authorization: Bearer $TOKEN" # saldo do plano
curl -s -X POST "$BASE/v1/mensagens" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: pedido-1234-pronto" -H "Content-Type: application/json" \
-d '{"destinatario":"(27) 99999-9999","texto":"Seu veículo está pronto.","consentimento":{"evidencia":"Cliente aceitou avisos no cadastro."}}'
curl -s "$BASE/v1/mensagens/<MENSAGEM_ID>" -H "Authorization: Bearer $TOKEN" # situação
PHP
<?php
// Configuração: guarde a chave em variável de ambiente, nunca no código.
const BASE = 'https://gateway.orquestrasistemas.com.br';
function orquestra(string $metodo, string $rota, ?array $corpo = null, ?string $token = null, ?string $idempotencia = null): array
{
$cabecalhos = [$token ? "Authorization: Bearer $token" : 'X-API-Key: ' . getenv('ORQUESTRA_CHAVE_API')];
if ($idempotencia) {
$cabecalhos[] = "Idempotency-Key: $idempotencia";
}
$ch = curl_init(BASE . $rota);
$opcoes = [CURLOPT_CUSTOMREQUEST => $metodo, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15];
if ($corpo !== null) {
// Só manda Content-Type quando há corpo (POST sem corpo com JSON vazio responde 400).
$cabecalhos[] = 'Content-Type: application/json';
$opcoes[CURLOPT_POSTFIELDS] = json_encode($corpo, JSON_UNESCAPED_UNICODE);
}
$opcoes[CURLOPT_HTTPHEADER] = $cabecalhos;
curl_setopt_array($ch, $opcoes);
$resposta = json_decode((string) curl_exec($ch), true) ?? [];
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception(($resposta['codigo'] ?? $status) . ': ' . ($resposta['title'] ?? ''));
}
return $resposta;
}
$token = orquestra('POST', '/v1/autenticacao/token')['token_acesso'];
$envio = orquestra('POST', '/v1/mensagens', [
'destinatario' => '(27) 99999-9999',
'texto' => 'Seu veículo está pronto para retirada.',
'consentimento' => ['evidencia' => 'Cliente aceitou avisos de serviço no cadastro.'],
], $token, 'pedido-1234-pronto');
echo $envio['mensagem_id'], PHP_EOL;
echo orquestra('GET', '/v1/mensagens/' . $envio['mensagem_id'], null, $token)['situacao'], PHP_EOL;
Python (3.10 ou superior, biblioteca requests)
import os, time
import requests
BASE = "https://gateway.orquestrasistemas.com.br"
_token = {"valor": None, "expira": 0.0}
def obter_token() -> str:
if _token["valor"] and time.time() < _token["expira"] - 30:
return _token["valor"]
r = requests.post(f"{BASE}/v1/autenticacao/token", headers={"X-API-Key": os.environ["ORQUESTRA_CHAVE_API"]}, timeout=15)
r.raise_for_status()
dados = r.json()
_token.update(valor=dados["token_acesso"], expira=time.time() + dados["expira_em_segundos"])
return dados["token_acesso"]
def enviar_mensagem(destinatario: str, texto: str, idempotencia: str) -> dict:
r = requests.post(
f"{BASE}/v1/mensagens",
headers={"Authorization": f"Bearer {obter_token()}", "Idempotency-Key": idempotencia},
json={"destinatario": destinatario, "texto": texto},
timeout=15,
)
if r.status_code != 202:
erro = r.json()
raise RuntimeError(f"{erro.get('codigo')}: {erro.get('title')}")
return r.json() # { mensagem_id, situacao: 'na_fila', consulta, ... }
aceita = enviar_mensagem("(27) 99999-9999", "Seu veículo está pronto.", "pedido-1234-pronto")
situacao = requests.get(f"{BASE}{aceita['consulta']}", headers={"Authorization": f"Bearer {obter_token()}"}, timeout=15)
print(aceita["mensagem_id"], situacao.json()["situacao"])
Node.js (18 ou superior)
const BASE = 'https://gateway.orquestrasistemas.com.br';
let token = null;
let expiraEm = 0;
async function obterToken() {
if (token && Date.now() < expiraEm - 30_000) return token;
const r = await fetch(`${BASE}/v1/autenticacao/token`, {
method: 'POST',
headers: { 'X-API-Key': process.env.ORQUESTRA_CHAVE_API },
});
if (!r.ok) throw new Error(`Token: ${r.status}`);
const dados = await r.json();
token = dados.token_acesso;
expiraEm = Date.now() + dados.expira_em_segundos * 1000;
return token;
}
export async function enviarMensagem(destinatario, texto, idempotencia) {
const r = await fetch(`${BASE}/v1/mensagens`, {
method: 'POST',
headers: {
Authorization: `Bearer ${await obterToken()}`,
'Idempotency-Key': idempotencia,
'Content-Type': 'application/json',
},
body: JSON.stringify({ destinatario, texto }),
});
const dados = await r.json();
if (!r.ok) throw new Error(`${dados.codigo}: ${dados.title}`);
return dados; // { mensagem_id, situacao: 'na_fila', consulta, ... }
}
export async function consultarSituacao(idMensagem) {
const r = await fetch(`${BASE}/v1/mensagens/${idMensagem}`, {
headers: { Authorization: `Bearer ${await obterToken()}` },
});
return r.json(); // { situacao, motivo_fila, ... }
}
14. Boas práticas
- Uma chave por sistema; se um sistema sair, o gestor revoga só a chave dele. Peça só as permissões que o sistema usa.
- Gere a
Idempotency-Keya partir do seu próprio evento (pedido, boleto, aviso) para nunca duplicar mensagens; repita a mesma chave nas novas tentativas. - Reaproveite o token; renove uns 30 segundos antes de expirar e, se receber
401 TOKEN_INVALIDO, peça outro uma vez e repita. - Mande só mensagens que a pessoa espera receber, com consentimento registrado e finalidade honesta; muitas denúncias levam o WhatsApp a restringir o número. Respeite o descadastro.
- O
202não é entrega: acompanhe a situação ou use webhook. - Limites por minuto, hora e dia valem por número; quando atingidos, a mensagem espera na fila (
motivo_fila) em vez de falhar. Não tente "furar" a fila com várias chaves. - Para muitos avisos, prefira o lote (100 por chamada) a 100 chamadas separadas.
- Acompanhe
GET /v1/saldoe avise o responsável antes de chegar a zero. - Verifique números (
/v1/contatos/verificar) só quando fizer sentido, não em massa. - Webhook: confira a assinatura, recuse timestamps antigos, trate
evento_idrepetido e responda rápido. - Nunca grave a chave, o token, o segredo do webhook ou o QR em logs, URLs, no navegador ou no Git.
15. Quem acessa o quê
No painel, cada pessoa da organização tem um papel. O gestor do gateway (administração global) vê todas as organizações, gerencia planos, aprova pagamentos e acompanha a saúde de todos os números.
| Papel | Pode | Não pode |
|---|---|---|
| Proprietário | Tudo na organização: números e QR Code, chaves, aplicações, grupos, membros, webhooks, consentimentos, campanhas, contratação do plano, recebimento e exclusão de mensagens recebidas | — |
| Administrador | O mesmo que o proprietário no dia a dia | — |
| Operador | Enviar e cancelar mensagens, verificar números, ver mensagens, consentimentos, campanhas e mensagens recebidas, responder | Criar ou aprovar campanhas, criar ou ver chaves, gerenciar números, membros e plano; ver auditoria e pagamentos |
| Auditor | Ver tudo, inclusive chaves (só o prefixo), membros, auditoria e pagamentos; usar Testar nas chaves | Alterar qualquer coisa ou enviar mensagens |
| Suporte | Ver números, saúde do serviço, filas e consumo | Ver conteúdo de negócio (mensagens, recebidas, consentimentos, arquivos, campanhas) e chaves |
Na API, quem acessa é o sistema integrado, pela chave. A chave só enxerga a organização dela e, conforme o tipo, o número (individual) ou o grupo (geral) a que está ligada; mensagens, arquivos e campanhas só são vistos pela chave que os criou. Cada rota exige a permissão correspondente (tabela da seção 1). O segredo da chave é exibido uma única vez; o painel guarda só um resumo irreversível e o prefixo. Revogar a chave bloqueia na hora todos os tokens emitidos por ela.
Dados pessoais. Mensagens, mídias e mensagens recebidas ficam cifradas; mensagens recebidas são apagadas no fim do prazo de retenção ou a pedido (DELETE /v1/mensagens-recebidas/{id}). Os registros técnicos não guardam conteúdo das mensagens.
16. Todas as rotas
Base: https://gateway.orquestrasistemas.com.br. Todas, exceto a primeira, usam Authorization: Bearer <token>. Quando há duas permissões, qualquer uma basta.
| Método | Rota | Permissão | Resposta | Seção |
|---|---|---|---|---|
| POST | /v1/autenticacao/token |
Chave no X-API-Key |
200 | 2 |
| GET | /v1/credenciais/contexto |
Qualquer token | 200 | 3 |
| GET | /v1/diagnostico |
Qualquer token (destinatario: envio) |
200 | 3 |
| POST | /v1/mensagens |
messages.send |
202 | 4 |
| GET | /v1/mensagens |
messages.read |
200 | 7 |
| POST | /v1/mensagens/lote |
messages.send |
202 | 4.1 |
| GET | /v1/mensagens/{mensagem_id} |
messages.read |
200 | 7 |
| POST | /v1/mensagens/{mensagem_id}/cancelar |
messages.cancel |
200 | 4.4 |
| GET | /v1/mensagens/{mensagem_id}/votos |
messages.read |
200 | 8.3 |
| POST | /v1/arquivos |
media.write |
202 | 4.2 |
| GET | /v1/arquivos/{arquivo_id} |
media.read |
200 | 4.2 |
| GET | /v1/arquivos/{arquivo_id}/conteudo |
media.read |
200 | 4.2 |
| POST | /v1/contatos/consentimentos |
consents.write |
201 | 6 |
| GET | /v1/contatos/consentimentos |
contatos:ler ou consents.write |
200 | 6 |
| POST | /v1/contatos/descadastros |
consents.write |
200 | 6 |
| POST | /v1/contatos/verificar |
messages.send |
200 | 4.3 |
| GET | /v1/instancias |
messages.read ou instancias:conectar |
200 | 8 |
| POST | /v1/instancias/{instancia_id}/conectar |
instances.connect |
200 | 8 |
| GET | /v1/instancias/{instancia_id}/qr |
instances.connect |
200 | 8 |
| POST | /v1/instancias/{instancia_id}/desconectar |
instances.connect |
200 | 8 |
| GET | /v1/grupos |
messages.read |
200 | 3 |
| GET | /v1/saldo |
messages.send ou analytics.read |
200 | 4.5 |
| GET | /v1/assinatura |
analytics.read |
200 | 4.5 |
| GET | /v1/limites |
analytics.read |
200 | 11 |
| GET | /v1/consumo |
analytics.read |
200 | 7 |
| GET | /v1/filas |
messages.read |
200 | 7 |
| GET | /v1/campanhas |
campaigns.write |
200 | 10 |
| POST | /v1/campanhas |
campaigns.write |
201 | 10 |
| POST | /v1/campanhas/{campanha_id}/iniciar |
campaigns.write |
202 | 10 |
| GET | /v1/mensagens-recebidas |
messages.receive |
200 | 8.1 |
| GET | /v1/mensagens-recebidas/{id} |
messages.receive |
200 | 8.1 |
| DELETE | /v1/mensagens-recebidas/{id} |
messages.receive |
204 | 8.1 |
| GET | /v1/mensagens-recebidas/{id}/midia |
messages.receive |
200 | 8.1 |
GET /v1/grupos devolve o grupo da chave geral (id, nome, politica, situacao); para chave individual, lista vazia.