How to Integrate the WhatsApp API in PHP (2026 Guide)

Last updated: June 3, 2026

Most WhatsApp API tutorials assume Node.js or Python. If you're working in PHP — Laravel, a plain script, or a legacy app — this guide gets you sending real WhatsApp messages from PHP in a few minutes, using nothing but cURL (built into PHP) or Guzzle. By the end you'll send your first message and receive replies via a webhook. No Meta Business verification, no template approval.

Prerequisites

  • PHP 7.4+ with the cURL extension (enabled by default in most installs).
  • An aulm account and a connected WhatsApp session (pair your number with a QR code from the dashboard).
  • Your API key (alm_sk_live_… or alm_sk_test_…) from Dashboard → API Keys, and your session UUID from Dashboard → Sessions.

Send your first WhatsApp message (plain cURL, zero dependencies)

The fastest way to send a WhatsApp message from PHP is the built-in cURL extension. No Composer, no packages.

<?php
$apiKey    = "alm_sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
$sessionId = "your-session-uuid";

$ch = curl_init("https://api.aulm.io/v1/messages/send-text");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        "Authorization: Bearer {$apiKey}",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "session_id" => $sessionId,
        "to"         => "14155550100",   // recipient, digits incl. country code
        "body"       => "Hello from PHP 👋",
    ]),
]);

$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status === 201) {
    echo "Message sent!\n";
    print_r(json_decode($response, true));
} else {
    echo "Failed (HTTP {$status}): {$response}\n";
}

A successful send returns 201 Created with a JSON body containing the message ID and queued status. The recipient phone should be digits only, including the country code, no + or spaces.

The cleaner way with Guzzle (Composer / Laravel)

If you already have Composer in the project (most Laravel apps do), Guzzle gives you typed exceptions and a nicer API.

<?php
require "vendor/autoload.php";

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$client = new Client(["base_uri" => "https://api.aulm.io"]);

try {
    $res = $client->post("/v1/messages/send-text", [
        "headers" => ["Authorization" => "Bearer alm_sk_live_xxx"],
        "json"    => [
            "session_id" => "your-session-uuid",
            "to"         => "14155550100",
            "body"       => "Hello from PHP via Guzzle",
        ],
    ]);
    echo "Sent: " . $res->getStatusCode();
} catch (RequestException $e) {
    echo "Error: " . $e->getResponse()?->getBody();
}

In Laravel you can drop this straight into a controller, or use Illuminate\Support\Facades\Http instead — same endpoint, same payload.

Handling responses and errors

Every aulm error response is JSON with a stable shape: { "error": { "code": "...", "message": "..." } }, plus an x-request-id response header. The status codes you'll actually see:

  • 201 Created — the message was accepted and queued for delivery.
  • 400 Bad Request — invalid JSON, missing fields, body over 4096 chars, or a malformed phone number.
  • 401 Unauthorized — missing, malformed, revoked, or expired API key.
  • 403 Forbidden — this API key isn't allowed to use this session (per-key session whitelist).
  • 404 Not Found — the session_id doesn't exist or isn't owned by your account.
  • 429 Too Many Requests — rate limit hit; back off and retry.

When you contact support, always include the x-request-id header from the failing response — it lets us find the exact request in our logs.

Check a session is connected before sending

WhatsApp sessions can drop (phone offline, logout, etc.). A quick status check avoids spending retries on a disconnected session.

<?php
$ch = curl_init("https://api.aulm.io/v1/sessions/your-session-uuid/status");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["Authorization: Bearer alm_sk_live_xxx"],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

// e.g. { "status": "connected" } — only send when connected
if (($result["status"] ?? null) !== "connected") {
    exit("Session not ready: " . ($result["status"] ?? "unknown"));
}

Receiving messages — a signed PHP webhook

To receive incoming WhatsApp messages, configure a webhook URL in the dashboard. aulm POSTs every event to that URL signed with HMAC-SHA256, using these headers:

  • x-aulm-signature: t=<unix-ts>,v1=<hex-hmac>
  • x-aulm-event — event type (e.g. message.received)
  • x-aulm-delivery-id — unique delivery ID (use it to dedupe)
  • x-aulm-attempt — attempt number for retries

The signature is computed as HMAC_SHA256(secret, "{t}.{rawBody}"). Always verify against the raw request body — not the parsed JSON, and not a re-encoded version — and always use a constant-time comparison.

<?php
// webhook.php — receives incoming WhatsApp messages from aulm
$secret = "your_webhook_signing_secret"; // shown once when you create the webhook
$raw    = file_get_contents("php://input");
$sigHdr = $_SERVER["HTTP_X_AULM_SIGNATURE"] ?? "";

// header format: "t=<unix>,v1=<hex>"
$parts = [];
foreach (explode(",", $sigHdr) as $kv) {
    [$k, $v] = array_pad(explode("=", $kv, 2), 2, "");
    $parts[$k] = $v;
}
$ts  = $parts["t"]  ?? "";
$sig = $parts["v1"] ?? "";

$expected = hash_hmac("sha256", "{$ts}.{$raw}", $secret);

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit("invalid signature");
}

// (Optional) reject events older than 5 minutes to prevent replay
if (abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit("stale timestamp");
}

$event = json_decode($raw, true);
// $event["event_type"], $event["data"], etc.
file_put_contents("incoming.log", print_r($event, true), FILE_APPEND);

http_response_code(200);
echo "ok";

Respond 2xx within a few seconds. Any non-2xx response triggers a retry with exponential backoff, so make your handler idempotent — use x-aulm-delivery-id to skip events you've already processed.

Next steps