Send and receive WhatsApp from PHP and Laravel
Receiving is a Laravel route that answers Meta's verification GET and accepts POSTs, reading the parameters as hub_mode, hub_verify_token and hub_challenge because PHP turns dots into underscores. Sending is one Http::withToken() POST to the Cloud API messages endpoint with the number's phone number id and token. No SDK is needed.
PHP converts dots in incoming variable names to underscores, so hub.challenge is read as hub_challenge.
Laravel's stateless API routes, created by install:api, served under /api and outside the web group's CSRF protection.
The Graph API version in Meta's current send message examples, at POST /{phone-number-id}/messages.
How do you receive WhatsApp messages in Laravel?
With a GET and a POST on the same path in routes/api.php. Run php artisan install:api first if the file does not exist. API routes are stateless and outside the web group, so Meta's POST is not rejected for a missing CSRF token, which is what happens on a route in routes/web.php.
The GET answers Meta's verification. Meta sends hub.mode, hub.verify_token and hub.challenge, but PHP rewrites dots in incoming variable names to underscores, so Laravel sees hub_mode, hub_verify_token and hub_challenge. Code that reads the dotted names gets null and never verifies. Return the challenge as plain text, not through response()->json().
The POST hands the payload to a queued job and returns 200 straight away. Put a long random key in the path and check it on both routes: Meta signs deliveries with the Tech Provider's app secret, so the signature header cannot be validated on your side.
use App\Jobs\HandleWhatsAppEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/whatsapp/{key}', function (Request $request, string $key) {
abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);
// PHP turns hub.mode into hub_mode: read the underscored names
if ($request->query('hub_mode') === 'subscribe'
&& hash_equals(config('services.whatsapp.verify_token'),
(string) $request->query('hub_verify_token'))) {
return response((string) $request->query('hub_challenge'), 200)
->header('Content-Type', 'text/plain');
}
abort(403);
});
Route::post('/whatsapp/{key}', function (Request $request, string $key) {
abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);
HandleWhatsAppEvent::dispatch($request->json()->all());
return response('', 200);
});How do you send a WhatsApp message from Laravel?
With Laravel's HTTP client: one POST to the messages endpoint for the phone number id, with the access token as a bearer token. Both values come from the connection, shown in the dashboard and returned by get_api_credentials, and belong in config/services.php backed by environment variables rather than in code.
The body shape depends on timing. Inside 24 hours of the customer's last message, a text object works, as below. Outside that window Meta accepts only a template object naming an approved template and its language, and free text fails with error 131047. The response is the place to see it: a failed call returns an error object whose code says what went wrong, so log it instead of discarding it.
Nothing in the request is specific to EasyCoexistence. It is the call in Meta's own documentation, so any PHP HTTP client, Guzzle or plain cURL, works the same way.
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.whatsapp.token'))
->post('https://graph.facebook.com/v25.0/'
. config('services.whatsapp.phone_number_id') . '/messages', [
'messaging_product' => 'whatsapp',
'to' => '14155550188', // digits only, with country code
'type' => 'text',
'text' => ['body' => 'Thanks, we are on it.'],
]);
if ($response->failed()) {
// error.code 131047: outside the window, send a template instead
logger()->warning('WhatsApp send failed', $response->json('error', []));
}What does a production Laravel handler need beyond that?
A real queue, deduplication, and a window check.
The queue matters because Meta retries a slow response. HandleWhatsAppEvent::dispatch() only returns quickly when the queue connection is a real one with a worker running. On the sync driver the job runs inside the request, and a slow model call or database write turns into a duplicate delivery.
Deduplicate on the message id, the wamid in each message, because retries are normal rather than exceptional. A unique index on that column, or a cache lock keyed on it, is enough.
And store when each contact last wrote. With that timestamp the send path picks text or template by itself, rather than finding out from error 131047 after the customer was already waiting. Replies typed on the phone in the WhatsApp Business app arrive as echo events on the same route, so the handler can also see when a person has taken over a conversation.
Common mistakes
- Reading hub.challenge with a dot. PHP renamed it to hub_challenge, so the handler returns an empty body and Meta never verifies.
- Putting the webhook in routes/web.php. Meta's POST has no CSRF token and is rejected with 419.
- Dispatching to the sync queue. The job runs before the 200, slow handling gets retried, and messages are processed twice.
- Returning the challenge with response()->json(). The quotes around the value fail Meta's check.
Connect the number at easycoexistence.com, set the webhook destination to your Laravel route with its key in the path, and read the phone number id and token from the dashboard. From US$ 9 per number per month, falling to US$ 1 as you add numbers, with the first 3 days free.
Frequently asked questions
Is there a PHP SDK?
Not from us, and none is needed. The API is Meta's, documented by Meta, and Laravel's HTTP client or Guzzle reaches it directly.
Does this work without Laravel?
Yes. In plain PHP read $_GET['hub_challenge'], echo it as text/plain, and send with cURL. The underscore rule applies to every PHP framework.
Why does the URL need a key in the path?
Because it is the authentication that works. Meta's signature uses the Tech Provider's app secret, which is never shared, so the URL secret is what keeps strangers from posting fake events.
Can the team still use the phone?
Yes. Coexistence keeps the WhatsApp Business app working on the number, and messages sent from the phone reach your route as echo events.
Keep reading
Ready to get started?
Set up WhatsApp Coexistence in minutes, not months. The app keeps working on the phone.
Start Free TrialVerified on