Download the PHP package dineshstack/laravel-whatsapp-cost-control without Composer
On this page you can find all versions of the php package dineshstack/laravel-whatsapp-cost-control. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download dineshstack/laravel-whatsapp-cost-control
More information about dineshstack/laravel-whatsapp-cost-control
Files in dineshstack/laravel-whatsapp-cost-control
Package laravel-whatsapp-cost-control
Short 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.
License MIT
Homepage https://github.com/dineshstack/laravel-whatsapp-cost-control
Informations about the package laravel-whatsapp-cost-control
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
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
Set the essentials in .env:
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
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.
When a send fails
Every result has success, status, body and message_id. A failed send adds why:
| Case | Extra keys |
|---|---|
| Meta refused it | error_code (e.g. 132001), error_title, error_message |
| Blocked before Meta | blocked: true, blocked_reason: country_not_allowed, budget_exhausted or recipient_suppressed |
| Meta did not answer | unreachable: true — the message may still have been delivered, so do not retry blindly |
Opt-outs
Every send checks opt-outs. Scope all blocks everything except AUTHENTICATION; scope marketing blocks MARKETING only. They are filled automatically when a customer replies a stop keyword (whatsapp.opt_out.stop_keywords), stops marketing messages inside WhatsApp (user_preferences webhook), or Meta refuses a send with 131050. Only a hash and the last four digits are stored. Changes dispatch WhatsAppRecipientSuppressed and WhatsAppRecipientRestored.
sendTemplate()'s opt-out check is scoped by the $category you pass it — pass 'MARKETING' for marketing sends so a marketing-scope opt-out is actually checked. Omit $category (or call isSuppressed() directly with none) and only an all-scope opt-out blocks the send; a marketing-scope opt-out on that recipient is silently not checked — isSuppressed() logs a warning when that happens so it is visible instead of just missed.
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.
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.
Subscribe your Meta app to messages, message_template_status_update, template_category_update and user_preferences. Template verdicts dispatch WhatsAppTemplateStatusChanged and recategorisations dispatch WhatsAppTemplateCategoryChanged; the package keeps no template state, so listen for these if you do. Meta redelivers webhooks, so both can fire more than once for the same verdict — listeners should be idempotent.
WhatsAppTemplateService::create() returns template_id, template_status and template_category — the category Meta assigned, which can differ from the one you asked for.
Admin API and permissions
Each admin endpoint checks one ability through $user->can(), so it works with plain Gates, spatie/laravel-permission, or any policy setup. By default reads need whatsapp.view and writes need whatsapp.manage; remap any ability in config('whatsapp.permissions') (the full list is Dineshstack\WhatsApp\Support\WhatsAppPermissions::DEFAULTS). Abilities you leave out keep their default. Middleware is yours:
Console
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
The incidents behind each design decision in this package, written up in order. Start with the first — it is the argument the rest of the package follows from.
- WhatsApp Cloud API sends you no invoice until it is too late — why the send response carries no price, why the pricing webhook carries no amount either, and the estimate-then-reconcile ledger that answers "what did this cost" at all.
missing_rateover a silently wrong number. - OTP pumping: the fraud that bills you for every code you send — the attacker never enters the code; the send is the transaction. Why rate limiting does not cap the loss and a fail-closed country allowlist does.
- Block the campaign at the cap. Never block the one-time code — the budget guard's category asymmetry. A broadcast overshooting is a cost problem; a refused OTP is a user who cannot log in.
- Verifying a WhatsApp webhook in Laravel: the three silent traps — PHP rewrites
hub.modetohub_modebefore your code sees it, HMAC over re-encoded JSON rejects genuine payloads, and Meta's 20-second window forces the work into a queue. - Random UUID keys fragment InnoDB. Ordered ones write clean — why the ledger and audit-log tables use
Str::orderedUuid(). The tables most likely to get UUID keys are the highest-write tables in the system.
More production Laravel at dineshstack.com.
License
MIT. See LICENSE.
Security
Vulnerabilities: [email protected], not a public issue.
Author
Dinesh Wijethunga — dineshstack.com
All versions of laravel-whatsapp-cost-control with dependencies
illuminate/support Version ^12.0|^13.0
illuminate/database Version ^12.0|^13.0
illuminate/http Version ^12.0|^13.0
illuminate/console Version ^12.0|^13.0
illuminate/queue Version ^12.0|^13.0
illuminate/routing Version ^12.0|^13.0