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ção | Método e caminho | Campos obrigatórios |
|---|---|---|
| Enviar texto | POST /api/v1/messages/send/text | sessionId, to, text |
| Enviar imagem | POST /api/v1/messages/send/image | sessionId, to, image (caption opcional) |
| Consultar sessão | GET /api/v1/sessions/{sessionId} | sessionId no caminho |
| Resposta de erro | qualquer rota | success, 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.
