Enviar WhatsApp con Java y HttpClient: ejemplo completo
Para enviar WhatsApp con Java haces un POST con java.net.http.HttpClient a /api/v1/{instanceId}/send/text, con las cabeceras x-api-key y x-api-secret y un JSON con number y text. HttpClient llegó con Java 11 y no requiere dependencias. Abajo tienes el ejemplo mínimo, el envío de multimedia, la versión asíncrona y una integración con Spring Boot.
Publicado el · 6 min de lectura
Este artículo también está disponible en: Português · English
Qué necesitas antes de abrir el IDE
Una cuenta en ZapZap, una instancia conectada por código QR, la clave y el secreto de la API y el ID de la instancia. La autenticación es por dos cabeceras. Necesitas Java 11 o superior por el java.net.http.HttpClient; los bloques de texto con tres comillas, usados abajo para el JSON, exigen Java 15 o superior.
Valida la instancia con cURL antes de programar. Si cURL funciona y Java no, el problema está en el código; si cURL falla, el problema está en la cuenta, la instancia o el 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."}'Cómo enviar un mensaje de texto con java.net.http.HttpClient
El programa de abajo arma el JSON, crea la petición con HttpRequest.newBuilder, define timeout y cabeceras y envía de forma síncrona. Crea el HttpClient una sola vez y reutilízalo: mantiene un pool de conexiones y es seguro entre hilos.
Escapar texto a mano es un error común. Si el mensaje puede traer comillas, saltos de línea o emojis, serializa con una biblioteca (Jackson o Gson) en vez de concatenar strings. El ejemplo usa una función mínima de escape solo para quedar sin dependencias.
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());
}
}Cómo enviar imagen, PDF y audio con Java
La ruta de multimedia es /send/media con number, type y file (URL pública), más text para la leyenda y docName para el nombre del documento. Los tipos son image, video, audio, ptt, myaudio, ptv, document y sticker. Para un PDF, usa document; para un mensaje de voz, ptt.
El método de abajo reutiliza el mismo HttpClient. Con Jackson, cambia el JSON armado a mano por un ObjectMapper y un Map, que se encarga del escape por ti.
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);
}Cómo enviar sin bloquear el hilo con sendAsync
HttpClient tiene sendAsync, que devuelve un CompletableFuture. Eso permite disparar una notificación sin trabar el flujo principal y encadenar el tratamiento del resultado. Para volumen, no dispares miles de futures a la vez: limita la concurrencia con un Semaphore o un Executor de tamaño fijo y espacia los mensajes.
La razón es la misma en cualquier lenguaje: el comportamiento del número (una ráfaga idéntica a quien no te conoce) pesa más que la tecnología del cliente HTTP. La guía de antibloqueo detalla las prácticas.
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;
});Cómo integrar con Spring Boot
En Spring Boot, expón un bean con las credenciales tomadas de application.yml (o de variables de entorno) e inyéctalo donde lo necesites. Usa RestClient (Spring 6.1 o superior) o WebClient si ya está en el proyecto; el HttpClient del JDK también funciona y evita dependencias.
El patrón recomendado es que el controller encole el pedido de envío (base de datos, cola de mensajes) y un worker haga la llamada a WhatsApp. Así, una falla o lentitud del envío no tumba la petición del usuario y controlas el ritmo.
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);
}
}Qué errores esperar y cómo tratarlos
| Código | Causa probable | Acción |
|---|---|---|
| 400 | number sin código de país, JSON inválido o campo obligatorio faltante | Corregir el payload |
| 401 o 403 | x-api-key o x-api-secret incorrectos o ausentes | Revisar las dos cabeceras |
| 404 | ID de instancia incorrecto | Revisar la URL |
| 429 | Ritmo demasiado alto | Esperar y reintentar con backoff |
| 5xx o HttpTimeoutException | Servidor o red | Reintentar pocas veces con backoff |
| Sin error, pero el mensaje no llega | Instancia desconectada o número sin WhatsApp | Ver GET /instance/status y POST /chat/check |
Registra siempre el status y el cuerpo de la respuesta. El mensaje de error del servidor suele señalar el campo problemático. La referencia de send/text lista los campos aceptados, incluidos delay y digitando, que simulan un ritmo de escritura más natural.
Cómo verificar números por lotes antes de enviar
El endpoint POST /chat/check recibe un arreglo en numbers y devuelve, para cada ítem, isInWhatsapp y el jid. Verificar antes de una lista grande evita gastar envíos en números que no existen y genera un informe de contactos inválidos para corregir en el origen. Envía por lotes (por ejemplo 50 por llamada), no un número a la vez.
El ejemplo reutiliza el método post definido arriba. En un proyecto real, deserializa la respuesta con Jackson a una lista de objetos en vez de tratar el 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 + "]}");
}Cómo limitar la concurrencia con ExecutorService y Semaphore
Disparar miles de llamadas a la vez es el error más común de quien descubre sendAsync. Usa un Semaphore para limitar los envíos simultáneos y una pausa entre mensajes. En Java 21 o superior, los hilos virtuales vuelven simple este patrón, pero el límite sigue siendo necesario: el cuello de botella no es el hilo, es el comportamiento del número.
Los valores de abajo (3 simultáneos, pausa de 2 a 6 segundos) son un punto de partida tuyo. Para campañas reales, la cola POST /sender/simple programa y espacia los envíos en el servidor, con delayMin y delayMax, y saca ese trabajo de tu aplicación.
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 producción en Java
Un HttpClient único y reutilizado; timeouts de conexión y de petición; credenciales fuera del código; serialización con Jackson o Gson; reintentos solo para 429 y 5xx; concurrencia limitada; una cola entre tu aplicación y el envío; log del status y del cuerpo en las fallas; y un endpoint de webhook para recibir respuestas (mira la guía de webhook).
Para conocer todos los recursos, abre la página de API de WhatsApp y el OpenAPI, que sirve de base para generar un cliente Java tipado con las herramientas que prefieras.
Resumen rápido
- Con Java 11 o superior, java.net.http.HttpClient envía WhatsApp sin dependencia extra: un POST a /send/text con las dos cabeceras.
- Crea el HttpClient una vez y reutilízalo; define siempre timeout de conexión y de petición.
- Usa Jackson o Gson para armar el JSON cuando el texto pueda traer comillas, saltos de línea o emojis.
- sendAsync no bloquea el hilo, pero el volumen pide cola, límite de concurrencia e intervalo entre mensajes.
- En Spring Boot, el controller encola y un worker llama a la API.
Sigue leyendo
Lee también
Preguntas frecuentes
¿Qué versión de Java necesito para enviar WhatsApp por la API?
Java 11 o superior, porque java.net.http.HttpClient llegó en esa versión. El ejemplo con bloque de texto de tres comillas pide Java 15 o superior; en versiones anteriores, arma el JSON con Jackson o con concatenación cuidadosa.
¿Cómo autenticar las llamadas en Java?
Con las cabeceras x-api-key y x-api-secret en cada petición, agregadas en HttpRequest.newBuilder. Lee los valores de variables de entorno o de application.yml, nunca de código versionado.
¿Necesito Retrofit, OkHttp o Apache HttpClient?
No. El HttpClient del JDK alcanza. Si tu proyecto ya usa OkHttp o el RestClient de Spring, las llamadas son las mismas: cambia solo la capa HTTP, manteniendo URL, cabeceras y cuerpo JSON.
¿Cómo envío un archivo PDF en Java?
POST a /send/media con number, type document, file con la URL pública del PDF, docName con el nombre mostrado y text opcional como leyenda.
¿Cómo sé si el mensaje fue entregado?
La respuesta del envío confirma que la API aceptó el mensaje. El estado de entrega y lectura llega por webhook, en el evento messages_update, si lo activas en la configuración del webhook de la instancia.
Dispara tu primer envío en Java
Crea la cuenta, verifica tu teléfono y recibe el crédito de bienvenida, sin depositar (uno por teléfono, sujeto a la campaña vigente). Conecta por código QR y ejecuta el ejemplo.
