API de WhatsApp em Java: HttpClient nativo e Spring Boot

Para integrar a API de WhatsApp em Java você não precisa de biblioteca extra: o HttpClient do Java 11 ou o RestClient do Spring Boot fazem o envio, e um controller com PostMapping recebe o webhook. Este guia mostra os dois caminhos com timeout configurado e tratamento do corpo de erro da API.

O contrato da API em termos de Java

Do ponto de vista do seu código, a API de WhatsApp da D-API é um serviço REST comum. Cada operação é uma requisição para https://api.d-api.cloud, com corpo JSON e o header Authorization contendo a API Key sem prefixo. Não há SDK Java, e não faz falta: basta modelar dois ou três records e escolher o cliente HTTP.

OperaçãoMétodo e caminhoCampos obrigatórios
Enviar textoPOST /api/v1/messages/send/textsessionId, to, text
Enviar imagemPOST /api/v1/messages/send/imagesessionId, to, image (caption opcional)
Consultar sessãoGET /api/v1/sessions/{sessionId}sessionId no caminho
Resposta de erroqualquer rotasuccess, error, statusCode

Java puro com java.net.http.HttpClient

Em um serviço sem Spring, um batch ou uma aplicação desktop, o cliente nativo do JDK resolve. Crie uma única instância de HttpClient e reaproveite; ela mantém o pool de conexões. Repare nos dois timeouts: o do builder limita o tempo para abrir a conexão, e o do request limita a espera pela resposta.

import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.Map;

public final class DApiClient {
    private static final String BASE = "https://api.d-api.cloud";
    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(3))
            .build();
    private final ObjectMapper json = new ObjectMapper();
    private final String apiKey;

    public DApiClient(String apiKey) { this.apiKey = apiKey; }

    public void sendText(String sessionId, String to, String text) throws Exception {
        String body = json.writeValueAsString(Map.of("sessionId", sessionId, "to", to, "text", text));
        HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/api/v1/messages/send/text"))
                .timeout(Duration.ofSeconds(10))
                .header("Authorization", apiKey)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();

        HttpResponse<String> res;
        try {
            res = http.send(req, HttpResponse.BodyHandlers.ofString());
        } catch (HttpTimeoutException e) {
            throw new DApiException(408, "sem resposta dentro do timeout");
        }
        if (res.statusCode() >= 400) {
            ApiError err = json.readValue(res.body(), ApiError.class);
            throw new DApiException(res.statusCode(), err.error());
        }
    }

    public record ApiError(boolean success, String error, int statusCode) {}

    public static final class DApiException extends RuntimeException {
        public final int status;
        public DApiException(int status, String message) { super(message); this.status = status; }
    }
}

O Jackson aparece só para serializar o JSON; se o projeto já usa Gson, a troca é direta. O HttpTimeoutException é lançado quando o tempo do request esgota, e convém tratá-lo à parte: nesse caso a mensagem pode ter sido entregue mesmo sem a resposta chegar.

Spring Boot com RestClient

No Spring Boot 3.2 ou superior, o RestClient é o jeito idiomático de chamar APIs de forma síncrona. Declare um bean configurado com URL base, header e uma request factory com timeouts, e injete onde precisar.

@Configuration
class DApiConfig {
    @Bean
    RestClient dapiRestClient(@Value("${dapi.api-key}") String apiKey) {
        var factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(Duration.ofSeconds(3));
        factory.setReadTimeout(Duration.ofSeconds(10));
        return RestClient.builder()
                .baseUrl("https://api.d-api.cloud")
                .defaultHeader("Authorization", apiKey)
                .requestFactory(factory)
                .build();
    }
}

@Service
class WhatsAppService {
    private final RestClient dapi;
    WhatsAppService(RestClient dapiRestClient) { this.dapi = dapiRestClient; }

    record SendImage(String sessionId, String to, String image, String caption) {}

    void enviarComprovante(String sessionId, String to, String urlImagem) {
        dapi.post()
            .uri("/api/v1/messages/send/image")
            .contentType(MediaType.APPLICATION_JSON)
            .body(new SendImage(sessionId, to, urlImagem, "Seu comprovante"))
            .retrieve()
            .onStatus(HttpStatusCode::isError, (req, res) -> {
                String corpo = new String(res.getBody().readAllBytes());
                throw new DApiException(res.getStatusCode().value(), corpo);
            })
            .toBodilessEntity();
    }
}

O onStatus transforma qualquer 4xx ou 5xx em uma exceção sua, com o corpo da API, que traz o motivo no campo error. Uma falha de rede ou tempo esgotado chega como ResourceAccessException. Separar as duas ajuda a decidir o que merece nova tentativa.

Receber o webhook com @PostMapping

Todo evento chega com o mesmo envelope, então um record com JsonNode no campo data dá conta de todos os tipos. O controller valida o token do caminho, entrega o evento a um serviço assíncrono e responde. Como o webhook de WhatsApp não é assinado, esse token secreto é a principal barreira contra chamadas falsas.

record WebhookEvent(String event, String sessionId, String timestamp, String traceId, JsonNode data) {}

@RestController
class WhatsAppWebhookController {
    private final EventoProcessor processor;
    private final String token;

    WhatsAppWebhookController(EventoProcessor processor, @Value("${dapi.webhook-token}") String token) {
        this.processor = processor;
        this.token = token;
    }

    @PostMapping("/webhooks/whatsapp/{token}")
    ResponseEntity<Void> receber(@PathVariable("token") String recebido, @RequestBody WebhookEvent evento) {
        if (!MessageDigest.isEqual(token.getBytes(), recebido.getBytes())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        if ("messages.received".equals(evento.event()) && !evento.data().path("fromMe").asBoolean()) {
            processor.processar(evento); // método @Async, roda em outro executor
        }
        return ResponseEntity.ok().build();
    }
}

Lembre de habilitar @EnableAsync e de definir um executor com fila limitada. Em aplicações com mais de uma instância, ou quando o processamento chama outros sistemas lentos, troque o @Async por uma fila persistente. A lista de eventos e o que vem em cada um estão em webhook de WhatsApp.

Monitorar a conexão pelo próprio webhook

Em sistemas corporativos, o incidente mais comum não é uma mensagem com erro, e sim um número que parou de funcionar sem ninguém perceber. O evento connection.status resolve isso: ele chega no mesmo endpoint, com data.status valendo connected, disconnected ou logged_out. No processor, trate esse evento antes das mensagens:

  • disconnected: registre e acompanhe. Quedas curtas acontecem, e a própria D-API tenta restabelecer a conexão.
  • logged_out: o aparelho saiu da sessão e alguém precisa ler o QR Code de novo. Esse é o caso para abrir alerta no seu canal de plantão ou avisar o cliente dono do número.
  • connected: feche o alerta e libere os envios que ficaram retidos, se a sua aplicação os segurou.

Transformar esses eventos em métrica, com um contador por status no Micrometer, deixa o painel do Grafana mostrando quantas conexões estão saudáveis a qualquer momento, sem polling na API.

Oficial ou não oficial, o mesmo código

Muitos times Java trabalham em empresas que avaliam a API oficial da Meta por exigência de compliance. Na D-API, o tipo é escolhido na criação da sessão (unofficial ou cloud_api), e o sessionId resultante funciona nas mesmas rotas de envio mostradas acima. A diferença está nas regras: para iniciar conversa na oficial é preciso template aprovado pela Meta. A comparação completa está em API oficial vs não oficial, e o produto em API oficial de WhatsApp. Para quem integra WhatsApp em sistemas de gestão, o que costuma ser o caso em Java, vale ler também sobre API de WhatsApp para ERP.

Perguntas frequentes

Tem SDK Java da D-API no Maven Central?

Não. A API é REST e qualquer cliente HTTP do ecossistema Java atende: o HttpClient nativo, RestClient, WebClient, OkHttp ou Feign. Os exemplos desta página usam só o que vem no JDK e no Spring Boot.

Funciona em Java 8?

O java.net.http.HttpClient só existe a partir do Java 11. Em Java 8 a integração continua possível com HttpURLConnection, Apache HttpClient ou OkHttp, com o mesmo endpoint, header e corpo JSON.

RestClient ou WebClient no Spring?

Se a aplicação é Spring MVC tradicional, RestClient, disponível a partir do Spring Framework 6.1, é mais simples e síncrono. WebClient faz sentido quando a aplicação inteira já é reativa com WebFlux.

O código muda se eu usar a API oficial da Meta?

O envio de texto e mídia continua na mesma rota, com o sessionId da conexão oficial. O que muda é iniciar conversa: na API oficial fora da janela de atendimento é preciso usar template aprovado, pela rota de template.

Como processar o webhook sem prender a thread do Tomcat?

Responda 200 logo depois de validar e mande o evento para um executor, um método anotado com Async ou uma fila. No Java 21, um executor de virtual threads é uma opção leve para esse trabalho.

Teste a API de WhatsApp da D-API

Trial de 3 dias com acesso completo. Sem cartão, sem fidelidade.