Send WhatsApp with Java and HttpClient: a complete example
To send WhatsApp with Java, you POST with java.net.http.HttpClient to /api/v1/{instanceId}/send/text with the x-api-key and x-api-secret headers and a JSON body with number and text. HttpClient arrived in Java 11 and needs no dependencies. Below are the minimal example, media sending, the async version and a Spring Boot integration.
Published on · 5 min read
What you need before opening the IDE
A ZapZap account, an instance connected by QR code, the API key and secret and the instance ID. Authentication is two headers. You need Java 11 or later for java.net.http.HttpClient; the triple-quote text blocks used below for the JSON need Java 15 or later.
Validate the instance with cURL before coding. If cURL works and Java does not, the problem is in the code; if cURL fails, the problem is in the account, the instance or the destination number.
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."}'How to send a text message with java.net.http.HttpClient
The program below builds the JSON, creates the request with HttpRequest.newBuilder, sets a timeout and headers and sends synchronously. Create the HttpClient once and reuse it: it keeps a connection pool and is thread-safe.
Escaping text by hand is a common mistake. If the message may contain quotes, line breaks or emoji, serialize with a library (Jackson or Gson) instead of concatenating strings. The example uses a minimal escape function only to stay dependency-free.
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());
}
}How to send images, PDFs and audio with Java
The media route is /send/media with number, type and file (public URL), plus text for the caption and docName for the document name. Types are image, video, audio, ptt, myaudio, ptv, document and sticker. For a PDF use document; for a voice message use ptt.
The method below reuses the same HttpClient. With Jackson, swap the hand-built JSON for an ObjectMapper and a Map, which handles escaping for you.
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);
}How to send without blocking the thread using sendAsync
HttpClient has sendAsync, which returns a CompletableFuture. That lets you fire a notification without blocking the main flow and chain the result handling. For volume, do not fire thousands of futures at once: bound concurrency with a Semaphore or a fixed-size Executor and space out messages.
The reason is the same in any language: how the number behaves (an identical burst to people who do not know you) weighs more than the HTTP client's technology. The anti-ban guide details the practices.
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;
});How to integrate with Spring Boot
In Spring Boot, expose a bean with credentials from application.yml (or environment variables) and inject it where needed. Use RestClient (Spring 6.1 or later) or WebClient if it is already in the project; the JDK HttpClient also works and avoids dependencies.
The recommended pattern is for the controller to enqueue the send request (database, message queue) and a worker to call WhatsApp. That way a failure or slowdown in sending does not take down the user's request, and you control the pace.
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);
}
}Which errors to expect and how to handle them
| Code | Likely cause | Action |
|---|---|---|
| 400 | number without country code, invalid JSON or missing required field | Fix the payload |
| 401 or 403 | Wrong or missing x-api-key or x-api-secret | Check both headers |
| 404 | Wrong instance ID | Check the URL |
| 429 | Pace too high | Wait and retry with backoff |
| 5xx or HttpTimeoutException | Server or network | Retry a few times with backoff |
| No error, but the message does not arrive | Disconnected instance or number without WhatsApp | Check GET /instance/status and POST /chat/check |
Always log the status and the response body. The server error message usually points to the problem field. The send/text reference lists the accepted fields, including delay and typing, which simulate a more natural typing rhythm.
How to check numbers in batches before sending
The POST /chat/check endpoint takes an array in numbers and returns, for each item, isInWhatsapp and the jid. Checking before a large list avoids spending sends on numbers that do not exist and produces a report of invalid contacts to fix at the source. Send in batches (for example 50 per call), not one number at a time.
The example reuses the post method defined above. In a real project, deserialize the response with Jackson into a list of objects instead of handling the 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 + "]}");
}How to limit concurrency with ExecutorService and Semaphore
Firing thousands of calls at once is the most common mistake of people who discover sendAsync. Use a Semaphore to limit simultaneous sends and a pause between messages. On Java 21 or later, virtual threads make this pattern simple, but the limit is still needed: the bottleneck is not the thread, it is the number's behavior.
The values below (3 at a time, a 2 to 6 second pause) are your starting point. For real campaigns, the POST /sender/simple queue schedules and spaces sends on the server, with delayMin and delayMax, and takes that work out of your application.
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;
}));
}
}Final checklist for Java in production
A single, reused HttpClient; connection and request timeouts; credentials outside the code; serialization with Jackson or Gson; retries only for 429 and 5xx; bounded concurrency; a queue between your application and the send; logging of status and body on failures; and a webhook endpoint to receive replies (see the webhook guide).
To explore all features, open the WhatsApp API page and the OpenAPI file, which can be the basis for generating a typed Java client with the tools you prefer.
Quick summary
- With Java 11 or later, java.net.http.HttpClient sends WhatsApp with no extra dependency: a POST to /send/text with the two headers.
- Create the HttpClient once and reuse it; always set connection and request timeouts.
- Use Jackson or Gson to build the JSON when the text may contain quotes, line breaks or emoji.
- sendAsync does not block the thread, but volume needs a queue, bounded concurrency and a pause between messages.
- In Spring Boot, the controller enqueues and a worker calls the API.
Keep reading
Also read
Frequently asked questions
Which Java version do I need to send WhatsApp through the API?
Java 11 or later, because java.net.http.HttpClient arrived in that version. The example with a triple-quote text block needs Java 15 or later; on earlier versions, build the JSON with Jackson or careful concatenation.
How do I authenticate calls in Java?
With the x-api-key and x-api-secret headers on every request, added in HttpRequest.newBuilder. Read the values from environment variables or application.yml, never from versioned code.
Do I need Retrofit, OkHttp or Apache HttpClient?
No. The JDK HttpClient is enough. If your project already uses OkHttp or Spring's RestClient, the calls are the same: only swap the HTTP layer, keeping URL, headers and JSON body.
How do I send a PDF file in Java?
POST to /send/media with number, type document, file with the PDF's public URL, docName with the displayed name and optional text as a caption.
How do I know the message was delivered?
The send response confirms the API accepted the message. Delivery and read status arrive by webhook, in the messages_update event, if you enable it in the instance's webhook configuration.
Fire your first send in Java
Create your account, verify your phone and get the welcome credit, no deposit needed (one per phone, subject to the current campaign). Connect by QR code and run the example.
