All integration guides

Java integration

Send WhatsApp messages with Java

A complete, runnable Java example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — Java talks to the API over ordinary HTTP, so you can paste this straight into an existing project.

About 5 minutes to your first delivered message

1. Setup

Create an API key in the dashboard under Accounts → your account → API keys, then make it available to your Java code as an environment variable. Never commit the key or ship it in client-side code.

# Java 11+ includes java.net.http.HttpClient — no dependency required.
export SPWP_API_KEY="wak_live_your_key_here"

2. Send a text message

One POST with a recipient and a body. The API replies 202 Accepted with a message id in QUEUED state; delivery happens asynchronously and you can track it by id or webhook.

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 SpwpClient {

    private static final String API_BASE = "https://api.spwp.app/api/v1";
    private final HttpClient http = HttpClient.newHttpClient();

    public String sendText(String to, String text) throws Exception {
        String payload = """
            {"to": "%s", "text": "%s"}
            """.formatted(to, text);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE + "/public/messages/text"))
                .header("X-API-Key", System.getenv("SPWP_API_KEY"))
                .header("Content-Type", "application/json")
                .timeout(Duration.ofSeconds(10))
                .POST(HttpRequest.BodyPublishers.ofString(payload))
                .build();

        HttpResponse<String> response =
                http.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() >= 400) {
            throw new IllegalStateException("SPWP error: " + response.body());
        }
        return response.body();
    }
}

3. Send an image, document or other media

Media endpoints take the same shape, with a source that is either a public HTTPS URL we fetch server-side or a base64 data URL you upload inline. Swap the path segment for video, audio, document or location.

public String sendImage(String to, String source, String caption) throws Exception {
    String payload = """
        {"to": "%s", "source": "%s", "caption": "%s"}
        """.formatted(to, source, caption);

    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_BASE + "/public/messages/image"))
            .header("X-API-Key", System.getenv("SPWP_API_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();

    return http.send(request, HttpResponse.BodyHandlers.ofString()).body();
}

4. Receive replies and delivery receipts

Inbound messages and status changes arrive as signed POSTs to your webhook URL. Recompute the HMAC over the timestamp and the raw request body — re-serialised JSON will not match — and reject anything older than five minutes.

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;

public final class SpwpSignature {

    public static boolean isValid(String rawBody, String timestamp,
                                  String signatureHeader, String secret)
            throws Exception {

        String v1 = signatureHeader.replaceAll(".*v1=([a-f0-9]+).*", "$1");

        // Replay protection: five-minute window.
        long age = Math.abs(System.currentTimeMillis() - Long.parseLong(timestamp));
        if (age > 5 * 60_000) return false;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] digest = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));

        String expected = HexFormat.of().formatHex(digest);
        return MessageDigest.isEqual(
                expected.getBytes(StandardCharsets.UTF_8),
                v1.getBytes(StandardCharsets.UTF_8));
    }
}

Error handling

Errors carry a non-2xx status and a stable code string you can branch on: VALIDATION_FAILED, INVALID_API_KEY, ACCOUNT_NOT_CONNECTED, RATE_LIMITED, MEDIA_FETCH_FAILED. Retry on 429 while honouring Retry-After; treat 401 as a configuration problem rather than a transient one.

Next steps

The full reference covers every endpoint, field and error code, plus the webhook payload shapes.

Other languages and frameworks