Enviar WhatsApp com Java e HttpClient: exemplo completo
Para enviar WhatsApp com Java você faz um POST com java.net.http.HttpClient em /api/v1/{instanceId}/send/text, com os cabeçalhos x-api-key e x-api-secret e um JSON com number e text. O HttpClient entrou no Java 11 e dispensa dependências. Abaixo estão o exemplo mínimo, o envio de mídia, a versão assíncrona e uma integração com Spring Boot.
Publicado em · 6 min de leitura
O que você precisa antes de abrir a IDE
Uma conta no ZapZap, uma instância conectada por QR code, a chave e o segredo da API e o ID da instância. A autenticação é por dois cabeçalhos. O Java exigido é a versão 11 ou superior, por causa do java.net.http.HttpClient; os blocos de texto com três aspas, usados abaixo para o JSON, exigem Java 15 ou superior.
Valide a instância com cURL antes de codar. Se o cURL funciona e o Java não, o problema está no código; se o cURL falha, o problema está na conta, na instância ou no número de destino.
cURL
curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/send/text \
-H "Content-Type: application/json" \
-H "x-api-key: SUA_API_KEY" \
-H "x-api-secret: SEU_API_SECRET" \
-d '{"number":"5511999999999","text":"Olá! Teste de envio com Java."}'Como enviar uma mensagem de texto com java.net.http.HttpClient
O programa abaixo monta o JSON, cria a requisição com HttpRequest.newBuilder, define timeout e cabeçalhos e envia de forma síncrona. Crie o HttpClient uma vez e reutilize: ele mantém um pool de conexões e é seguro entre threads.
Escapar texto à mão é um erro comum. Se a mensagem puder ter aspas, quebras de linha ou emojis, serialize com uma biblioteca (Jackson ou Gson) em vez de concatenar strings. O exemplo usa uma função mínima de escape apenas para ficar sem dependências.
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class EnviarWhatsApp {
static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
static String escapar(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
}
public static void main(String[] args) throws Exception {
String instancia = System.getenv("ZAPZAP_INSTANCE_ID");
String url = "https://api.zapzapapi.com/api/v1/" + instancia + "/send/text";
String json = """
{"number":"5511999999999","text":"%s"}
""".formatted(escapar("Olá! Seu pedido saiu para entrega."));
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.header("x-api-key", System.getenv("ZAPZAP_API_KEY"))
.header("x-api-secret", System.getenv("ZAPZAP_API_SECRET"))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 300) {
throw new IllegalStateException("Failed " + res.statusCode() + ": " + res.body());
}
System.out.println(res.body());
}
}Como enviar imagem, PDF e áudio com Java
A rota de mídia é /send/media com number, type e file (URL pública), mais text para legenda e docName para o nome do documento. Os tipos são image, video, audio, ptt, myaudio, ptv, document e sticker. Para um PDF, use document; para uma mensagem de voz, ptt.
O método abaixo reaproveita o mesmo HttpClient. Com Jackson, troque o JSON montado à mão por um ObjectMapper e um Map, que cuida do escape para você.
Java
static String post(String caminho, String json) throws Exception {
String url = "https://api.zapzapapi.com/api/v1/" + System.getenv("ZAPZAP_INSTANCE_ID") + caminho;
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(30))
.header("Content-Type", "application/json")
.header("x-api-key", System.getenv("ZAPZAP_API_KEY"))
.header("x-api-secret", System.getenv("ZAPZAP_API_SECRET"))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 300) {
throw new IllegalStateException("Failed " + res.statusCode() + ": " + res.body());
}
return res.body();
}
static String enviarDocumento(String numero, String urlPdf, String nome, String legenda) throws Exception {
String json = "{\"number\":\"" + numero + "\",\"type\":\"document\",\"file\":\"" + urlPdf
+ "\",\"docName\":\"" + nome + "\",\"text\":\"" + escapar(legenda) + "\"}";
return post("/send/media", json);
}Como enviar sem bloquear a thread com sendAsync
O HttpClient tem sendAsync, que devolve um CompletableFuture. Isso permite disparar uma notificação sem travar o fluxo principal e encadear o tratamento do resultado. Para volume, não dispare milhares de futures de uma vez: limite a concorrência com um Semaphore ou um Executor de tamanho fixo e espace as mensagens.
A razão é a mesma de qualquer linguagem: o comportamento do número (rajada idêntica para quem não conhece você) pesa mais do que a tecnologia do cliente HTTP. O guia de anti-bloqueio detalha as práticas.
Java
HTTP.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(res -> {
if (res.statusCode() >= 300) {
throw new IllegalStateException("Failed " + res.statusCode() + ": " + res.body());
}
return res.body();
})
.thenAccept(corpo -> System.out.println("sent: " + corpo))
.exceptionally(e -> {
System.err.println("error: " + e.getMessage());
return null;
});Como integrar com Spring Boot
Em Spring Boot, exponha um bean com as credenciais vindas do application.yml (ou de variáveis de ambiente) e injete onde precisar. Use RestClient (Spring 6.1 ou superior) ou WebClient se já estiver no projeto; o HttpClient do JDK também funciona e evita dependências.
O padrão recomendado é o controller enfileirar o pedido de envio (banco, fila de mensagens) e um worker fazer a chamada ao WhatsApp. Assim, uma falha ou lentidão do envio não derruba a requisição do usuário e você controla a velocidade.
Java
@Service
public class ZapZapService {
private final RestClient rest;
public ZapZapService(@Value("${zapzap.instance-id}") String instancia,
@Value("${zapzap.api-key}") String key,
@Value("${zapzap.api-secret}") String secret) {
this.rest = RestClient.builder()
.baseUrl("https://api.zapzapapi.com/api/v1/" + instancia)
.defaultHeader("x-api-key", key)
.defaultHeader("x-api-secret", secret)
.build();
}
public String enviarTexto(String numero, String texto) {
return rest.post().uri("/send/text")
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("number", numero, "text", texto))
.retrieve()
.body(String.class);
}
}Quais erros esperar e como tratar
| Código | Causa provável | Ação |
|---|---|---|
| 400 | number sem DDI, JSON inválido ou campo obrigatório faltando | Corrigir o payload |
| 401 ou 403 | x-api-key ou x-api-secret errados ou ausentes | Conferir os dois cabeçalhos |
| 404 | ID da instância errado | Conferir a URL |
| 429 | Ritmo alto demais | Esperar e repetir com backoff |
| 5xx ou HttpTimeoutException | Servidor ou rede | Repetir poucas vezes com backoff |
| Sem erro, mas a mensagem não chega | Instância desconectada ou número sem WhatsApp | Ver GET /instance/status e POST /chat/check |
Registre sempre o status e o corpo da resposta. A mensagem de erro do servidor costuma apontar o campo problemático. A referência de send/text lista os campos aceitos, incluindo delay e digitando, que simulam um ritmo mais natural de digitação.
Como verificar números em lote antes de enviar
O endpoint POST /chat/check recebe um array em numbers e devolve, para cada item, isInWhatsapp e o jid. Verificar antes de uma lista grande evita gastar envio com número que não existe e gera um relatório de contatos inválidos para corrigir na origem. Envie em lotes (por exemplo 50 por chamada) e não um número por vez.
O exemplo reaproveita o método post definido acima. Em um projeto real, desserialize a resposta com Jackson para uma lista de objetos em vez de tratar a string.
Java
static String verificar(java.util.List<String> numeros) throws Exception {
String lista = numeros.stream().map(n -> "\"" + n + "\"").collect(java.util.stream.Collectors.joining(","));
return post("/chat/check", "{\"numbers\":[" + lista + "]}");
}Como limitar a concorrência com ExecutorService e Semaphore
Disparar milhares de chamadas ao mesmo tempo é o erro mais comum de quem descobre o sendAsync. Use um Semaphore para limitar os envios simultâneos e uma pausa entre mensagens. Em Java 21 ou superior, threads virtuais deixam esse padrão simples, mas o limite continua necessário: o gargalo não é a thread, é o comportamento do número.
Os valores abaixo (3 simultâneos, pausa de 2 a 6 segundos) são ponto de partida seu. Para campanhas reais, a fila POST /sender/simple agenda e espaça os envios no servidor, com delayMin e delayMax, e tira esse trabalho da sua aplicação.
Java
static void enviarLista(java.util.Map<String, String> contatos) throws InterruptedException {
java.util.concurrent.Semaphore limite = new java.util.concurrent.Semaphore(3);
try (var pool = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
contatos.forEach((numero, texto) -> pool.submit(() -> {
try {
limite.acquire();
String json = "{\"number\":\"" + numero + "\",\"text\":\"" + escapar(texto) + "\"}";
post("/send/text", json);
Thread.sleep(2000 + (long) (Math.random() * 4000));
} catch (Exception e) {
System.err.println("failed for " + numero + ": " + e.getMessage());
} finally {
limite.release();
}
return null;
}));
}
}Checklist final para produção em Java
Um HttpClient único e reutilizado; timeouts de conexão e de requisição; credenciais fora do código; serialização com Jackson ou Gson; retry só para 429 e 5xx; concorrência limitada; fila entre a sua aplicação e o envio; log do status e do corpo de falhas; e um endpoint de webhook para receber respostas (veja o guia de webhook).
Para conhecer todos os recursos, abra a página de API de WhatsApp e o OpenAPI, que serve de base para gerar um cliente Java tipado com as ferramentas de sua preferência.
Resumo rápido
- Com Java 11 ou superior, java.net.http.HttpClient envia WhatsApp sem dependência extra: POST em /send/text com os dois cabeçalhos.
- Crie o HttpClient uma vez e reutilize; defina sempre timeout de conexão e de requisição.
- Use Jackson ou Gson para montar o JSON quando o texto puder ter aspas, quebras de linha ou emojis.
- sendAsync não bloqueia a thread, mas volume pede fila, limite de concorrência e intervalo entre mensagens.
- Em Spring Boot, o controller enfileira e um worker chama a API.
Continue lendo
Perguntas frequentes
Qual versão do Java preciso para enviar WhatsApp pela API?
Java 11 ou superior, porque o java.net.http.HttpClient entrou nessa versão. O exemplo com bloco de texto de três aspas pede Java 15 ou superior; em versões anteriores, monte o JSON com Jackson ou concatenação cuidadosa.
Como autenticar as chamadas em Java?
Com os cabeçalhos x-api-key e x-api-secret em toda requisição, adicionados no HttpRequest.newBuilder. Leia os valores de variáveis de ambiente ou do application.yml, nunca de código versionado.
Preciso de Retrofit, OkHttp ou Apache HttpClient?
Não. O HttpClient do JDK atende. Se o seu projeto já usa OkHttp ou RestClient do Spring, as chamadas são as mesmas: troque só a camada HTTP, mantendo URL, cabeçalhos e corpo JSON.
Como enviar um arquivo PDF em Java?
POST em /send/media com number, type document, file com a URL pública do PDF, docName com o nome exibido e text opcional como legenda.
Como sei se a mensagem foi entregue?
A resposta do envio confirma que a API aceitou a mensagem. O status de entrega e leitura chega por webhook, no evento messages_update, se você o ativar na configuração do webhook da instância.
Dispare o primeiro envio em Java
Crie a conta, verifique o telefone e receba o crédito de boas-vindas, sem depositar (um por telefone, sujeito à campanha vigente). Conecte por QR code e rode o exemplo.
