Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

How to Send WhatsApp Messages from Laravel 13 with the WhatsApp Cloud API

๐Ÿ› ๏ธ Laravel Tutorials5 min read

How to Send WhatsApp Messages from Laravel 13 with the WhatsApp Cloud API

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
How to Send WhatsApp Messages from Laravel 13 with the WhatsApp Cloud API

To send WhatsApp messages from Laravel 13, use the WhatsApp Cloud API from Meta. You send an authenticated POST request to the Graph API with the sender's phone number ID, the recipient's number and the message. With Laravel's HTTP client this takes one small service class. The most important rule: free-form text is only allowed inside the customer service window; outside it you must send an approved template.

Targets: Laravel 13 (PHP 8.3 to 8.5) and the WhatsApp Business Platform Cloud API. Meta updates API versions regularly, so the Graph version is configurable.

What do I need from Meta first?

  • A Meta developer account and an app with the WhatsApp product enabled.
  • A WhatsApp Business account with a phone number connected to the app (the dashboard provides a test number for development).
  • The Phone Number ID and an access token. Tokens created in the quick-start panel are temporary; for production create a permanent token through a system user.
  • While you use the test number, add the recipient numbers you want to message as allowed recipients in the dashboard.

Step 1: Add the settings

SQL
WHATSAPP_TOKEN=your-access-token
WHATSAPP_PHONE_NUMBER_ID=your-phone-number-id
WHATSAPP_GRAPH_VERSION=copy-the-current-version-from-the-meta-docs
JavaScript
'whatsapp' => [
    'token' => env('WHATSAPP_TOKEN'),
    'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'),
    'graph_version' => env('WHATSAPP_GRAPH_VERSION'),
],

Add that array to config/services.php. Keep the token only in .env.

Step 2: Build a service class

JavaScript
<?php

namespace App\Services;

use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class WhatsAppService
{
    private function send(array $payload): array
    {
        $url = sprintf(
            'https://graph.facebook.com/%s/%s/messages',
            config('services.whatsapp.graph_version'),
            config('services.whatsapp.phone_number_id')
        );

        try {
            return Http::withToken(config('services.whatsapp.token'))
                ->timeout(20)
                ->post($url, ['messaging_product' => 'whatsapp'] + $payload)
                ->throw()
                ->json();
        } catch (RequestException $e) {
            report($e);

            throw new RuntimeException('WhatsApp message could not be sent.');
        }
    }

    public function sendText(string $to, string $body): array
    {
        return $this->send([
            'to' => $to,
            'type' => 'text',
            'text' => ['body' => $body],
        ]);
    }

    public function sendTemplate(string $to, string $template, string $language = 'en_US', array $parameters = []): array
    {
        $components = $parameters === [] ? [] : [[
            'type' => 'body',
            'parameters' => array_map(fn ($value) => ['type' => 'text', 'text' => (string) $value], $parameters),
        ]];

        return $this->send([
            'to' => $to,
            'type' => 'template',
            'template' => [
                'name' => $template,
                'language' => ['code' => $language],
                'components' => $components,
            ],
        ]);
    }
}

Phone numbers go in international format with the country code and no +, spaces or dashes, for example 923001234567.

Step 3: Use it in a controller

JavaScript
<?php

namespace App\Http\Controllers;

use App\Services\WhatsAppService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use RuntimeException;

class WhatsAppController extends Controller
{
    public function send(Request $request, WhatsAppService $whatsapp): JsonResponse
    {
        $data = $request->validate([
            'to' => ['required', 'regex:/^[1-9][0-9]{7,14}$/'],
            'message' => ['required', 'string', 'max:1000'],
        ]);

        try {
            $result = $whatsapp->sendText($data['to'], $data['message']);
        } catch (RuntimeException $e) {
            return response()->json(['message' => $e->getMessage()], 502);
        }

        return response()->json($result);
    }
}
CSS
Route::post('/whatsapp/send', [WhatsAppController::class, 'send'])->middleware(['auth', 'throttle:10,1']);

The regex accepts 8 to 15 digits that do not start with zero, which matches the E.164 shape without the plus sign.

Text messages versus template messages

WhatsApp only allows a free-form text message when the user wrote to your number within the last 24 hours. To start a conversation, or to write after that window, you must send a pre-approved template. Create templates in the WhatsApp Manager, wait for approval, then call sendTemplate() with the exact template name and language code. The sample hello_world template is available for the test number.

Shell
$whatsapp->sendTemplate('923001234567', 'order_update', 'en_US', ['A-1042', 'tomorrow']);

How do I test this without sending real messages?

JavaScript
use Illuminate\Support\Facades\Http;

Http::fake(['graph.facebook.com/*' => Http::response(['messages' => [['id' => 'wamid.TEST']]])]);

$result = app(\App\Services\WhatsAppService::class)->sendText('923001234567', 'Hello');

Http::assertSent(fn ($request) => $request['type'] === 'text' && $request['to'] === '923001234567');

How do I verify it works for real?

  1. Confirm the recipient is on the allowed list of your test number.
  2. Call the endpoint with your own number and the hello_world template.
  3. Check the response for a messages array with an ID, then check the WhatsApp app on the phone.

Common mistakes

  • Using + or spaces in the number: send digits only, with the country code.
  • Sending text outside the 24-hour window: use a template instead.
  • Expired temporary token: you get an authentication error. Use a permanent system-user token in production.
  • Recipient not allowed: with the test number you can only message numbers you added in the dashboard.
  • Messaging people who did not agree: WhatsApp expects opt-in. Only message users who consented.

FAQ

Is the WhatsApp Cloud API free?

Pricing depends on the conversation or message type and your country. Check Meta's current pricing page before launching.

Can I receive messages too?

Yes, with a webhook. Meta calls a URL on your Laravel app when a message arrives, and you verify the request with the verify token you configured.

Do I need a package?

No. The HTTP client is enough for sending, and the service class keeps the code easy to replace.

References

Article Index & Topic Tags
LaravelApiHttp-ClientWhatsapp

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.