Dev

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

Este artigo também está disponível em: English · Español

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ódigoCausa provávelAção
400number sem DDI, JSON inválido ou campo obrigatório faltandoCorrigir o payload
401 ou 403x-api-key ou x-api-secret errados ou ausentesConferir os dois cabeçalhos
404ID da instância erradoConferir a URL
429Ritmo alto demaisEsperar e repetir com backoff
5xx ou HttpTimeoutExceptionServidor ou redeRepetir poucas vezes com backoff
Sem erro, mas a mensagem não chegaInstância desconectada ou número sem WhatsAppVer 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.

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.