All integration guides

PHP integration

Send WhatsApp messages with Laravel

A complete, runnable Laravel example for sending WhatsApp messages through the SPWP REST API. There is no SDK to install — Laravel 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 Laravel code as an environment variable. Never commit the key or ship it in client-side code.

# Laravel's HTTP client is built in — nothing to require.

# .env
SPWP_API_KEY=wak_live_your_key_here
SPWP_WEBHOOK_SECRET=your_webhook_secret

# config/services.php
'spwp' => [
    'key' => env('SPWP_API_KEY'),
    'webhook_secret' => env('SPWP_WEBHOOK_SECRET'),
    'base' => 'https://api.spwp.app/api/v1',
],

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.

<?php
// app/Services/Spwp.php

namespace App\Services;

use Illuminate\Support\Facades\Http;

class Spwp
{
    public function sendText(string $to, string $text): array
    {
        $response = Http::withHeaders([
                'X-API-Key' => config('services.spwp.key'),
            ])
            ->timeout(10)
            ->acceptJson()
            ->post(config('services.spwp.base') . '/public/messages/text', [
                'to' => $to,
                'text' => $text,
            ])
            ->throw();

        return $response->json('data');
    }
}

// Anywhere in your app:
//   app(Spwp::class)->sendText('9647503505440', "Order #{$order->id} shipped.");

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.

<?php
// app/Jobs/SendWhatsAppInvoice.php — send media from a queued job so a slow
// upload never blocks the request.

namespace App\Jobs;

use App\Services\Spwp;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Http;

class SendWhatsAppInvoice implements ShouldQueue
{
    use Dispatchable, Queueable;

    public function __construct(
        public string $phone,
        public string $pdfUrl,
        public string $fileName,
    ) {}

    public function handle(): void
    {
        Http::withHeaders(['X-API-Key' => config('services.spwp.key')])
            ->post(config('services.spwp.base') . '/public/messages/document', [
                'to' => $this->phone,
                'source' => $this->pdfUrl,
                'fileName' => $this->fileName,
                'mimeType' => 'application/pdf',
            ])
            ->throw();
    }
}

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.

<?php
// app/Http/Middleware/VerifySpwpSignature.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;

class VerifySpwpSignature
{
    public function handle(Request $request, Closure $next)
    {
        $ts = $request->header('X-WA-Timestamp');
        preg_match('/v1=([a-f0-9]+)/', $request->header('X-WA-Signature', ''), $m);
        $v1 = $m[1] ?? '';

        abort_if(!$ts || !$v1, 400);
        abort_if(abs(now()->getTimestampMs() - (int) $ts) > 5 * 60000, 400);

        $expected = hash_hmac(
            'sha256',
            $ts . '.' . $request->getContent(),
            config('services.spwp.webhook_secret'),
        );

        abort_unless(hash_equals($expected, $v1), 401);

        return $next($request);
    }
}

// routes/api.php — exclude from CSRF, the signature is the auth.
// Route::post('/webhooks/spwp', SpwpWebhookController::class)
//     ->middleware(VerifySpwpSignature::class);

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