Enviar WhatsApp com Python é uma das integrações mais pedidas por times de produto: confirmação de pedido, lembrete de consulta, código de verificação, aviso de boleto vencendo. Com a APIBrasil, isso vira uma chamada HTTP feita com a biblioteca requests, sem automatizar o WhatsApp Web na mão e sem manter um navegador aberto no seu servidor.
O tutorial parte do zero: você cria a conta, gera o Bearer Token, conecta um dispositivo e faz a primeira chamada. Depois evolui para imagem e arquivo, aprende a ler o campo error (que pode vir com HTTP 200) e acompanha o status de entrega. No fim, sai com um script reutilizável para colocar no seu projeto.
Todo o código roda em Python 3.10 ou superior. Os endpoints foram conferidos na documentação oficial em doc.apibrasil.io; quando um campo opcional não aparece na doc, o texto avisa para você confirmar antes de usar em produção.
O que você vai construir
O resultado é um módulo para enviar WhatsApp com Python com três funções: enviar texto, enviar arquivo (imagem, PDF ou áudio) e verificar se o dispositivo está conectado. Ele lê as credenciais de variáveis de ambiente, reaproveita a conexão HTTP entre chamadas e devolve o campo data da resposta ou lança uma exceção com a mensagem que a API retornou.
O fluxo tem cinco etapas, e cada seção cobre uma delas:
- Gerar o Bearer Token no painel e guardá-lo em variável de ambiente.
- Criar um dispositivo WhatsApp, obter o DeviceToken e conectar o número via QR Code.
- Fazer o POST em /whatsapp/sendText com requests.
- Tratar o campo error e os códigos HTTP de autenticação e saldo.
- Acompanhar o status de entrega (enviado, recebido, lido) pelos webhooks do dispositivo.
Se o seu time trabalha com JavaScript, existe um guia equivalente para criar um bot de WhatsApp com Node.js no Programador Brasil. A autenticação e os endpoints são os mesmos; muda só a linguagem.
Pré-requisitos: conta, token e dispositivo
Você precisa de três coisas antes de escrever código: uma conta na APIBrasil, um Bearer Token e um dispositivo WhatsApp conectado. As APIs de WhatsApp são cobradas por plano, e não por crédito, e exigem o header DeviceToken além do Authorization.
1. Conta e Bearer Token
Crie a conta em platform.apibrasil.com.br e gere o token no painel. Ele vai no header Authorization: Bearer em toda chamada, de qualquer API do catálogo. Se esta é sua primeira integração, o guia de primeiros passos com Bearer Token e homologação mostra o painel tela por tela.
2. Dispositivo e DeviceToken
No painel, contrate um plano de WhatsApp e crie um dispositivo. Cada dispositivo tem o próprio DeviceToken, que identifica qual número vai disparar a mensagem. Sem esse header, a API recusa a requisição, mesmo com o Bearer Token certo.
3. Conectar o número
Com o dispositivo criado, inicie a sessão com POST /whatsapp/start, informando um nome em session e qrcode: true. A resposta traz o QR Code para ler no celular, em Dispositivos conectados. Quando o status muda para inChat, o número está pronto para enviar e receber. Se preferir código de pareamento em vez de QR Code, envie o telefone no campo number.
4. Ambiente Python
python -m venv .venv && source .venv/bin/activate
pip install requests python-dotenv
# .env (nunca versione este arquivo)
APIBRASIL_TOKEN=seu_bearer_token
APIBRASIL_DEVICE_TOKEN=token_do_dispositivo
Tokens nunca entram no código-fonte. Leia-os com os.environ e mantenha o .env fora do repositório; em produção, use o cofre de segredos da sua plataforma.
Primeira chamada: enviar WhatsApp com Python e requests
Toda chamada à APIBrasil é um POST com JSON para a base https://gateway.apibrasil.io/api/v2. Para texto, o endpoint é /whatsapp/sendText e o corpo tem dois campos obrigatórios: number (telefone só com dígitos, com código do país e DDD) e text (a mensagem).
import os
import requests
from dotenv import load_dotenv
load_dotenv()
BASE_URL = "https://gateway.apibrasil.io/api/v2"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['APIBRASIL_TOKEN']}",
"DeviceToken": os.environ["APIBRASIL_DEVICE_TOKEN"],
}
def enviar_texto(numero: str, texto: str) -> dict:
"""Envia texto. numero no formato 55 + DDD + telefone, só dígitos."""
resposta = requests.post(
f"{BASE_URL}/whatsapp/sendText",
headers=HEADERS,
json={"number": numero, "text": texto},
timeout=30,
)
corpo = resposta.json()
# A API pode responder HTTP 200 com error = true: confira sempre o campo.
if resposta.status_code != 200 or corpo.get("error"):
raise RuntimeError(f"Falha no envio: {corpo.get('message')}")
return corpo
if __name__ == "__main__":
print(enviar_texto("5531999999999", "Olá! Seu pedido #1234 foi aprovado."))
A resposta segue o padrão de todas as APIs da plataforma: error (booleano), message (texto legível), data (o retorno da operação) e, nas APIs por crédito, balance e tax. O detalhe que mais derruba integração é o erro vir com HTTP 200: por isso o código testa resposta.status_code e corpo[“error”] juntos, e nunca só um deles.
Antes de um disparo em lote, confira se o dispositivo está online com POST /whatsapp/isConnected (sem corpo, mesmos headers). Se ele responder que não há conexão, não adianta enfileirar mensagens; o número precisa ser reconectado pelo QR Code.

Enviando texto, imagem e arquivo
A API WhatsApp WPP tem um endpoint para cada tipo de conteúdo. Para a maior parte dos casos, dois resolvem: sendText para texto e sendFile para qualquer mídia hospedada em uma URL pública (imagem, PDF, planilha). A tabela resume o que foi confirmado na documentação:
| Endpoint | Campos do corpo | Uso |
|---|---|---|
| POST /whatsapp/sendText | number, text (opcionais: time_typing, options) | Texto simples; time_typing simula digitação em ms |
| POST /whatsapp/sendFile | number, path | Imagem, documento ou vídeo a partir de uma URL pública |
| POST /whatsapp/sendFile64 | confira na documentação | Mesmo envio, com o arquivo em base64 |
| POST /whatsapp/sendAudio | number, path | Áudio a partir de URL |
| POST /whatsapp/sendText/queue | igual ao sendText | Envio assíncrono; devolve o id de um job |
Como os endpoints compartilham headers e formato de resposta, vale centralizar a chamada em uma função genérica e deixar cada tipo de envio como um atalho:
def chamar(endpoint: str, payload: dict) -> dict:
"""POST genérico: valida HTTP e o campo error da APIBrasil."""
resposta = requests.post(
f"{BASE_URL}/whatsapp/{endpoint}", headers=HEADERS, json=payload, timeout=60
)
corpo = resposta.json()
if resposta.status_code != 200 or corpo.get("error"):
raise RuntimeError(f"{endpoint}: {corpo.get('message', resposta.text)}")
return corpo
def enviar_arquivo(numero: str, url_publica: str) -> dict:
"""Imagem, PDF ou vídeo. A URL precisa ser acessível pela internet."""
return chamar("sendFile", {"number": numero, "path": url_publica})
def enviar_audio(numero: str, url_publica: str) -> dict:
return chamar("sendAudio", {"number": numero, "path": url_publica})
# exemplos
enviar_arquivo("5531999999999", "https://meusite.com.br/notas/nf-1234.pdf")
enviar_arquivo("5531999999999", "https://meusite.com.br/img/promocao.jpg")
Três cuidados práticos. Primeiro, a URL em path tem que responder sem autenticação, porque é o servidor da APIBrasil que baixa o arquivo, não o seu script. Segundo, para arquivos gerados na hora (um PDF montado no request, por exemplo), use sendFile64 e confira na documentação o nome exato dos campos do base64. Terceiro, campos extras como legenda e nome de arquivo não aparecem na doc do sendFile; se precisar deles, teste no seu próprio número antes de assumir que funcionam.
Tratando erros e status de entrega
Um envio que “deu certo” na resposta HTTP significa apenas que a API aceitou a mensagem. Entre isso e o cliente ler o texto existem três pontos de falha: credenciais, saldo ou plano, e a entrega em si pelo WhatsApp.
Erros de autenticação, plano e servidor
- 401 ou 403: Bearer Token inválido, expirado ou DeviceToken faltando. Corrija a configuração; repetir a chamada não resolve.
- 402: saldo ou plano insuficiente. Também não adianta repetir; é preciso regularizar no painel.
- HTTP 200 com error: true: número mal formatado, dispositivo desconectado ou outro erro de negócio. A explicação está em message.
- 5xx ou timeout: instabilidade. Aqui sim vale repetir, com espera exponencial e poucas tentativas.
Nunca repita automaticamente respostas 4xx: além de não resolver, em APIs cobradas por chamada cada tentativa pode gerar custo. A política de retry abaixo segue essa regra.
import time
def chamar_com_retry(endpoint: str, payload: dict, tentativas: int = 3) -> dict:
for i in range(tentativas):
try:
resposta = requests.post(
f"{BASE_URL}/whatsapp/{endpoint}", headers=HEADERS, json=payload, timeout=30
)
except requests.Timeout:
time.sleep(2 ** i) # 1s, 2s, 4s
continue
if resposta.status_code >= 500:
time.sleep(2 ** i)
continue
corpo = resposta.json()
if resposta.status_code != 200 or corpo.get("error"):
raise RuntimeError(corpo.get("message", resposta.text)) # 4xx: não repete
return corpo
raise RuntimeError(f"{endpoint}: sem resposta após {tentativas} tentativas")
Status de entrega: enviado, recebido, lido
O status de entrega não vem na resposta do sendText; ele chega por webhook. Ao iniciar o dispositivo, você informa uma URL em wh_message, e a APIBrasil passa a enviar um POST a cada evento: SEND_MESSAGE quando a mensagem sai, MESSAGE_STATUS quando o WhatsApp confirma cada etapa (SENT, RECEIVED, READ, PLAYED para áudio) ou reporta falha (FAILED, EXPIRED, entre outros). O campo id do evento é o mesmo do envio, o que permite cruzar as duas pontas.
Se você usou a rota /queue, a resposta traz o id de um job. O endpoint GET /jobs lista os jobs da conta com status, created_at e processed_at; o resultado do envio em si continua chegando pelos eventos do dispositivo. Para a parte de servidor que recebe esses eventos, veja o tutorial de webhooks de WhatsApp e SMS com Node.js deste blog.
Depois que o envio está estável, o problema muda de lado: quem responde o cliente que respondeu? Vale monitorar o atendimento no WhatsApp com indicadores de tempo de resposta e conversas sem retorno, porque uma notificação bem entregue que gera uma pergunta sem resposta vira reclamação.
Script completo
Juntando tudo em uma classe para enviar WhatsApp com Python: sessão HTTP reaproveitada, retry só para 5xx e timeout, validação do campo error e os três envios. Salve como apibrasil_whatsapp.py e importe no seu projeto.
"""apibrasil_whatsapp.py - cliente mínimo para a API WhatsApp WPP da APIBrasil."""
import os
import re
import time
import requests
class ErroAPIBrasil(Exception):
pass
class WhatsAppAPIBrasil:
BASE_URL = "https://gateway.apibrasil.io/api/v2/whatsapp"
def __init__(self, token: str | None = None, device_token: str | None = None):
self.sessao = requests.Session()
self.sessao.headers.update({
"Content-Type": "application/json",
"Authorization": f"Bearer {token or os.environ['APIBRASIL_TOKEN']}",
"DeviceToken": device_token or os.environ["APIBRASIL_DEVICE_TOKEN"],
})
@staticmethod
def normalizar(numero: str) -> str:
digitos = re.sub(r"\D", "", numero)
return digitos if digitos.startswith("55") else f"55{digitos}"
def _post(self, endpoint: str, payload: dict | None = None, tentativas: int = 3) -> dict:
for i in range(tentativas):
try:
r = self.sessao.post(f"{self.BASE_URL}/{endpoint}", json=payload or {}, timeout=30)
except requests.Timeout:
time.sleep(2 ** i)
continue
if r.status_code >= 500:
time.sleep(2 ** i)
continue
corpo = r.json()
if r.status_code != 200 or corpo.get("error"):
raise ErroAPIBrasil(f"{endpoint} ({r.status_code}): {corpo.get('message', r.text)}")
return corpo
raise ErroAPIBrasil(f"{endpoint}: sem resposta após {tentativas} tentativas")
def conectado(self) -> bool:
corpo = self._post("isConnected")
return not corpo.get("error")
def texto(self, numero: str, texto: str) -> dict:
return self._post("sendText", {"number": self.normalizar(numero), "text": texto})
def arquivo(self, numero: str, url: str) -> dict:
return self._post("sendFile", {"number": self.normalizar(numero), "path": url})
def audio(self, numero: str, url: str) -> dict:
return self._post("sendAudio", {"number": self.normalizar(numero), "path": url})
if __name__ == "__main__":
wa = WhatsAppAPIBrasil()
if not wa.conectado():
raise SystemExit("Dispositivo desconectado: leia o QR Code novamente.")
wa.texto("(31) 99999-9999", "Olá! Sua consulta é amanhã às 14h.")
wa.arquivo("(31) 99999-9999", "https://meusite.com.br/docs/orientacoes.pdf")
Para rodar, exporte as duas variáveis de ambiente, ajuste o número de teste para o seu e execute python apibrasil_whatsapp.py. O método conectado() usa o próprio campo error como sinal; se a sua versão da API devolver o estado em data, adapte a leitura conforme a resposta que você observar.
Perguntas frequentes
Preciso da API oficial do WhatsApp para usar esse tutorial?
Não. O tutorial usa a API WhatsApp WPP da APIBrasil, que conecta um número comum pelo QR Code, do mesmo jeito que o WhatsApp Web. A plataforma também oferece a API Oficial (Cloud) para quem precisa de conta verificada e templates aprovados; os conceitos de token e resposta são os mesmos.
Como devo formatar o número de destino?
Só dígitos, com código do país e DDD: 55 + DDD + telefone, como 5531999999999. Sem o +, sem parênteses e sem hífen. A função normalizar() do script faz essa limpeza e adiciona o 55 quando ele falta.
A resposta veio com error: false, mas a mensagem não chegou. O que verificar?
Primeiro, chame /whatsapp/isConnected para confirmar que o dispositivo está online. Depois, acompanhe os eventos MESSAGE_STATUS no webhook: se o status parou em SENT, o celular do destinatário pode estar sem internet; se veio FAILED, o número pode não ter WhatsApp ativo. Testar com o seu próprio número elimina boa parte das dúvidas.
Posso enviar para uma lista grande de números?
Pode, mas com cuidado. Use a rota /queue para não bloquear o script, respeite o limite do seu plano e envie apenas para quem autorizou receber mensagens. Disparos em massa sem opt-in levam ao bloqueio do número pelo próprio WhatsApp, e nenhum retry resolve isso.
Dá para testar sem gastar?
A homologação com homolog: true vale para as APIs de consulta por crédito, como CPF e CNPJ. No WhatsApp, o jeito seguro é enviar para o seu próprio número a partir do dispositivo de teste. Mantenha um dispositivo separado para desenvolvimento, assim um bug não dispara mensagens para clientes reais.
Próximos passos
Com o módulo pronto, o próximo passo natural é receber as respostas dos clientes e reagir a elas, o que pede um servidor de webhooks e, muitas vezes, um chatbot com menu. Se parte do time usa JavaScript, a versão deste guia para enviar WhatsApp com Node.js usa os mesmos endpoints. E se ainda não tem conta, crie a sua em platform.apibrasil.com.br, conecte um dispositivo de teste e rode o script acima com o seu número antes de ligar qualquer fluxo em produção.
![]()









