Dev

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ódigoCausa probableAcción
400number sin código de país, JSON inválido o campo obligatorio faltanteCorregir el payload
401 o 403x-api-key o x-api-secret incorrectos o ausentesRevisar las dos cabeceras
404ID de instancia incorrectoRevisar la URL
429Ritmo demasiado altoEsperar y reintentar con backoff
5xx o HttpTimeoutExceptionServidor o redReintentar pocas veces con backoff
Sin error, pero el mensaje no llegaInstancia desconectada o número sin WhatsAppVer 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.

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.