Key Takeaways
- A Laravel webhook receiver is a POST route that HMAC-verifies X-SmsGateway-Signature on the raw body, persists Event-Id, returns 200, and dispatches a job.
- There is no official Composer “Complete Laravel SDK.” Illuminate HTTP talks REST HTTPS/JSON. Confirm headers in Developer Center.
- VerifyCsrfToken must except the webhook path. CSRF is browser protection, not a substitute for HMAC.
- message.delivered is DLR. message.received is inbound. message.failed is a send-path failure. Different parsers.
- Do not run STOP lists or OTP state machines on the request thread. Horizon exists so the gateway does not mark you unhealthy.
Teams search laravel android sms gateway api receive webhook after the first DLR hits a 419. Laravel’s CSRF middleware treated the POST like a form. The gateway is not a browser. This Hub C recipe is the Laravel-shaped receiver: raw body, HMAC, job. Hub: Android SMS gateway API. Product surface: SMS webhook integration. Live fields stay in the Developer Center.
You need a working Android phone with a SIM and SMS credit from your mobile operator. Operator message costs are yours—we do not sell carrier SMS balance. Service pricing is based on device count and total SMS sent through the gateway. See device and SMS volume pricing.
v1=HMAC-SHA256
Horizon job
200 first. Parser second. CRM never on the FPM thread.
Laravel HTTP, not a Laravel SDK
Illuminate’s HTTP client is a client. It is not a vendor SDK, a Composer package we ship, or a replacement for OpenAPI. Sending still uses Bearer JSON to POST /api/v1/messages. Receiving still uses the headers documented under webhooks. PHP HTTPS samples: PHP REST samples. Laravel’s own HTTP docs: HTTP client.
Laravel’s CSRF middleware is for browsers. Gateway webhooks are not browsers. Except the path, then HMAC the bytes.
CSRF exception is not auth
A 419 on POST /webhooks/sms-gateway usually means VerifyCsrfToken ate the request. Except that path. Keep CSRF on login, billing, and every other form. The webhook is authenticated by HMAC, not by a session cookie. If you “fix” 419 by turning CSRF off globally, you have a different incident.
// App\Http\Middleware\VerifyCsrfToken — conceptual
protected $except = [
'webhooks/sms-gateway',
];Prefer a dedicated route file that never loads the web middleware group. Confirm the live register URL at https://app.sms-gateway.app/api/v1/webhooks in Developer Center — this blog does not own the catalog.
HMAC the raw body
X-SmsGateway-Signature is v1= hex of HMAC-SHA256 over {timestamp}.{body}. Timestamp is X-SmsGateway-Timestamp (unix seconds). Reject skew beyond about five minutes. Use hash_equals. Do not pretty-print, do not run the body through$request->all() before you sign.
Clock skew after a deploy is a common false 401. NTP on the app hosts is part of the webhook, not “ops later.”
ACK 200, then a job
Gateways retry when you hang. A controller that talks to Stripe, Zendesk, and a STOP list on the same request will look like an outage. Persist Event-Id, return 200, dispatch. Horizon failed-jobs is where STOP parsers belong. Two-way product: two-way SMS. STOP handling: STOP and opt-outs.
DLR vs inbound vs failed
| type | Means | Laravel job should |
|---|---|---|
| message.delivered | Operator DLR when the radio got one | Mark the OTP/order delivered; never overwrite with a late Pending |
| message.failed | Send path failed | Alert; do not infinite-retry the same challenge |
| message.received | Inbound on the SIM | Thread, STOP, or agent — separate parser from DLR |
Delivery reports also exist as polls: SMS delivery reports. One mapper for webhook and poll so precedence does not fork.
Payload and PHP verify
Illustrative delivered event. Confirm field names in OpenAPI when they disagree with this blog.
{
"id": "evt_01K2F8QW3N4RXB7M",
"type": "message.delivered",
"createdAt": "2026-08-12T14:04:09Z",
"apiVersion": "2026-08-12",
"data": {
"message": {
"id": 41823,
"number": "+14155552671",
"text": "Your verification code is 481920",
"status": "Delivered",
"campaignId": 17,
"deviceId": 3,
"metadata": { "orderId": "1234" },
"sentAt": "2026-08-12T14:04:02Z",
"deliveredAt": "2026-08-12T14:04:09Z"
}
}
}Conceptual PHP verify (same headers Laravel must implement):
<?php
header('Content-Type: application/json');
$secret = getenv('SMS_GATEWAY_WEBHOOK_SECRET');
$timestamp = $_SERVER['HTTP_X_SMSGATEWAY_TIMESTAMP'] ?? '';
$sigHeader = $_SERVER['HTTP_X_SMSGATEWAY_SIGNATURE'] ?? '';
$eventId = $_SERVER['HTTP_X_SMSGATEWAY_EVENT_ID'] ?? '';
$body = file_get_contents('php://input');
if ($timestamp === '' || $sigHeader === '' || $secret === '') {
http_response_code(400);
echo json_encode(['error' => 'missing_signature']);
exit;
}
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
echo json_encode(['error' => 'stale_timestamp']);
exit;
}
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$ok = false;
foreach (preg_split('/\s+/', $sigHeader) as $candidate) {
if (hash_equals($expected, $candidate)) {
$ok = true;
break;
}
}
if (!$ok) {
http_response_code(401);
echo json_encode(['error' => 'bad_signature']);
exit;
}
// Deduplicate on X-SmsGateway-Event-Id (retries and replays reuse it).
$data = json_decode($body, true) ?: [];
$type = $data['type'] ?? '';
$message = $data['data']['message'] ?? [];
if ($type === 'message.received') {
$from = $message['number'] ?? '';
$text = $message['text'] ?? '';
error_log("Inbound SMS $eventId from $from: $text");
}
http_response_code(200);
echo json_encode(['ok' => true]);
Java sibling for the same HMAC: receive webhooks in Java.
Ship checklist
- Webhook secret in the environment; rotated after contractor access.
- CSRF excepted on one path; CSRF still on everywhere else.
- Raw-body HMAC with skew window and Event-Id unique index.
- 200 before Horizon; failed jobs paged.
- OTP codes never logged. MSISDNs masked.
- Staff inbound canary on the Free 300 lifetime SMS allowance before production.
Next steps
Point staging at a signed URL, send a staff inbound, watch Event-Id land once. Device setup · Android app · Laravel SMS gateway hub. Laravel CSRF docs: CSRF protection.
Related product pages
Jump to the live product docs for this topic—not another long-form article.
- SMS webhook integrationInbound and status events
- SMS delivery reports (DLR)Delivery status tracking
- SMS API documentationLive endpoint reference
- PHP REST send samplesPHP code examples





