dineshstack / laravel-whatsapp-cost-control
WhatsApp Business Cloud API for Laravel with the part nobody packages: per-message cost tracking, country rate cards, budget guardrails that block runaway marketing spend, template management and a full send audit log.
Package info
github.com/dineshstack/laravel-whatsapp-cost-control
pkg:composer/dineshstack/laravel-whatsapp-cost-control
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
README
WhatsApp Business Cloud API for Laravel — with the part nobody packages: cost control.
Sending a WhatsApp message is one HTTP call. Knowing what your messages cost, per country and per category, and stopping a marketing campaign before it burns the month's budget — that is the part every production integration ends up building by hand. This package is that part, extracted from a live system, plus the sending.
- Send templates, OTPs and session text through Meta's Cloud API
- Per-message cost ledger — estimated cost at send time from a rate card, actual cost reconciled from Meta's pricing webhooks
- Country × category rate card, effective-dated, so Meta's re-pricing never rewrites history
- Budget guardrails — spend caps per period; MARKETING sends are blocked when the cap is hit, transactional traffic never is
- Country allowlist that fails closed — the cap on OTP-pumping fraud
- Template management API — list, create, update and delete templates against Meta
- Full audit log — every API call recorded with payloads, status and latency; Meta keeps no per-app call log, so this is the only audit trail your sends will ever have
- Admin REST API for all of it: costs, budgets, rates, health, profile, logs
Why not just use an existing WhatsApp package?
Existing packages cover sending well. None of them answer, in production:
- "How much did WhatsApp cost us this month, per country?"
- "Which sends have no rate configured, so their cost is unknown?"
- "Can a runaway campaign spend past our budget?" (Here: no — it gets blocked.)
- "What exactly did we send Meta at 03:12, and what did it answer?"
If you only need to fire messages, a lighter package is fine. This one is for when WhatsApp becomes a line item.
Requirements
| PHP | ^8.2 |
| Laravel | ^12.0 | ^13.0 |
| A Meta WhatsApp Business account | phone number ID, WABA ID, permanent token |
Installation
composer require dineshstack/laravel-whatsapp-cost-control php artisan vendor:publish --tag=whatsapp-config php artisan migrate
Set the essentials in .env:
WHATSAPP_PHONE_NUMBER_ID=123456789012345 WHATSAPP_WABA_ID=123456789012345 WHATSAPP_ACCESS_TOKEN=your-permanent-token WHATSAPP_APP_SECRET=your-app-secret WHATSAPP_VERIFY_TOKEN=any-random-string-you-choose # REQUIRED before anything sends — see "Fail-closed by default" below WHATSAPP_ALLOWED_COUNTRY_CODES=971,94 # Currency your dashboards and budgets speak (Meta bills USD) WHATSAPP_CURRENCY=AED WHATSAPP_CURRENCY_PER_USD=3.6725 WHATSAPP_TIMEZONE=Asia/Dubai
Fail-closed by default
Two settings block everything until you configure them, on purpose:
WHATSAPP_ALLOWED_COUNTRY_CODES— empty means no destination is allowed. The allowlist is the ceiling on OTP-pumping fraud; a misconfigured deploy should refuse to send, not send worldwide.- Marketing budgets — once a
MARKETINGbudget's cap is reached, marketing sends are blocked until the budget is raised. UTILITY, AUTHENTICATION and SERVICE messages are never blocked by budgets; they warn instead.
Sending
use Dineshstack\WhatsApp\Services\WhatsAppService; $wa = app(WhatsAppService::class); // A pre-approved template $wa->sendTemplate('971501234567', 'order_shipped', 'en_US', [ ['type' => 'body', 'parameters' => [['type' => 'text', 'text' => '#4821']]], ]); // OTP via the template configured in whatsapp.templates.otp $wa->sendOtp('971501234567', '482913'); // Free-form text (only lands inside an open 24h service window) $wa->sendText('971501234567', 'Your driver has arrived.');
Every send goes through one funnel: country allowlist → budget check → 15-second-bounded HTTP call → audit log row → cost-ledger row. A blocked send returns the same shape as a failed one, with blocked => true distinguishing policy from outage.
Cost tracking
At send time the package estimates cost from the rate card (country calling code × Meta category, effective-dated). When Meta's pricing webhooks arrive, the actual cost replaces the estimate. Messages with no configured rate surface in the cost summary as missing_rate — visibly unknown, never silently wrong.
GET .../costs/summary totals: estimated, actual, effective, missing_rate
GET .../costs/messages the ledger, filterable
GET .../rates the rate card
PUT .../rates upsert a rate (new effective_from, history intact)
GET .../budgets configured caps + current spend
PUT .../budgets set caps and alert thresholds
Seed a starter rate card for one country with WHATSAPP_SEED_RATE_COUNTRY=971 (values are BSP-published estimates — verify against Meta's own rate card and correct via PUT .../rates).
Webhooks
Point Meta at the webhook URL (default /api/whatsapp/webhook). The GET verify handshake and HMAC-SHA256 signature validation are handled; payloads are queued so the endpoint answers inside Meta's 20-second window. Status updates fill in delivery state and actual costs on the ledger.
Admin API and permissions
All admin endpoints (default prefix /api/v1/admin/whatsapp) authorize through two abilities: whatsapp.view and whatsapp.manage, resolved via $user->can() — so they work with plain Gates, spatie/laravel-permission, or any policy setup. Middleware is yours:
// config/whatsapp.php 'routes' => [ 'admin' => [ 'prefix' => 'api/v1/admin/whatsapp', 'middleware' => ['api', 'auth:sanctum'], ], ],
Console
php artisan whatsapp:test {phone} # end-to-end send check
php artisan whatsapp:templates # list templates as Meta sees them
php artisan whatsapp:prune-logs # trim the audit log (default 90 days)
Provenance
Extracted from a production booking platform where it sends OTPs and transactional messages daily — the budget guard, rate card and ledger exist because that system needed them, not speculatively. Client-specific code was removed; the send funnel, cost logic and tests are the production versions.
Further reading
- 5 Laravel Migration Mistakes That Made It Into Production — from the same production codebase discipline this package came out of
- More production Laravel at dineshstack.com
License
MIT. See LICENSE.
Security
Vulnerabilities: dineshlwuae@gmail.com, not a public issue.
Author
Dinesh Wijethunga — dineshstack.com