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
WHATSAPP_TOKEN=your-access-token
WHATSAPP_PHONE_NUMBER_ID=your-phone-number-id
WHATSAPP_GRAPH_VERSION=copy-the-current-version-from-the-meta-docs'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
<?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
<?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);
}
}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.
$whatsapp->sendTemplate('923001234567', 'order_update', 'en_US', ['A-1042', 'tomorrow']);How do I test this without sending real messages?
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?
- Confirm the recipient is on the allowed list of your test number.
- Call the endpoint with your own number and the
hello_worldtemplate. - Check the response for a
messagesarray 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.



