Looking to hire Laravel developers? Try LaraJobs

laravel-whatsapp-cost-control maintained by dineshstack

Description
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.
Last update
2026/08/11 18:58 (dev-main)
License
Downloads
1

Comments
comments powered by Disqus

Laravel WhatsApp Cost Control

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

Tests License: MIT


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 MARKETING budget'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

License

MIT. See LICENSE.

Security

Vulnerabilities: dineshlwuae@gmail.com, not a public issue.

Author

Dinesh Wijethungadineshstack.com