Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Whatsapp Laravel Package

sevenspan/laravel-whatsapp

A Laravel package for WhatsApp integration. Send text, media, and template messages through a simple fluent API, configure credentials in Laravel, and connect with notifications and webhooks. Streamlines WhatsApp Cloud API messaging for Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, single-maintainer wrapper around WhatsApp's sending APIs for Laravel. It has low stars and zero dependents, but it is not fully abandoned — v1.3.1 was published recently, adding support for dynamic components. That release resets the risk posture from "archived reference code" to "low-traffic package with a slow but real maintenance cadence." Still budget an hour to read the source before wiring it into anything customer-facing, and prefer vendoring/forking if your integration is long-lived.

Install and publish

composer require sevenspan/laravel-whatsapp
php artisan vendor:publish --provider="Sevenspan\WhatsApp\WhatsAppServiceProvider"

Pin the exact version in composer.json ("sevenspan/laravel-whatsapp": "1.3.1" or a caret constraint bounded to the current minor). Releases are infrequent and the changelog is terse, so a floating constraint can silently pull a behaviour change in a "patch." Lock it, upgrade deliberately.

Where to look first, in this order

  1. config/whatsapp.php — this is the real API surface. It tells you which drivers/providers are supported, which env keys each one needs, and whether the driver is chosen per-environment or per-call.
  2. The service provider — see what's bound in the container and whether there's a facade alias, a singleton manager, or a driver manager (Manager::driver('x') style). That determines whether you can extend it later.
  3. The driver classes themselves — the sending method signature (text vs. template vs. media vs. dynamic components) and the return shape (raw provider response? DTO? message ID string?). This is the single thing most likely to surprise you and the area most recently touched (see v1.3.1 notes).
  4. README last. Small packages frequently lag behind their own code; the config file is ground truth, and changelog entries ("dynamic component") are usually a one-liner that understates the actual payload shape change.

Your first use case: a template message to one number

Don't start with free-form text. Business-initiated WhatsApp messages outside a 24-hour customer-service window must be pre-approved templates, and that constraint lives in the provider, not the package. Create and get a template approved in the provider console first, then:

// Verify exact method name against the driver source — this is the shape to expect.
WhatsApp::to('919876543210')->template('order_shipped', ['ORD-1042'])->send();

If your template uses a dynamic URL button (the headline of v1.3.1), the approved template will define a placeholder that the send call must fill — confirm from the driver source whether the dynamic component is a top-level argument, part of the parameter array, or a separate builder method. If that path returns a provider message ID, you're done with the spike. Everything else — queuing, retries, webhooks, logging — is your job, not the package's.

Sanity check before you go further: fire one message in local against a test number, confirm the config keys actually load (php artisan tinker → config('whatsapp')), and confirm whether the driver throws exceptions or returns a falsy/error-flagged value on failure. That last detail dictates your entire error-handling strategy, so find it now rather than in production.

Implementation Patterns

Never call the facade from controllers. Wrap it once:

final class WhatsAppSender
{
    public function sendTemplate(string $to, string $template, array $params, array $dynamic = []): SentMessage
    {
        $response = WhatsApp::to($to)
            ->template($template, $params, $dynamic)
            ->send();

        return new SentMessage(
            providerId: $response->id ?? null,
            to: $to,
            raw: $response,
        );
    }
}

This gives you one place to swap the package out (see Gotchas — you probably will) and one place to normalise the inconsistent return shapes you'll find across drivers. Your application code should never see a provider-specific payload, and adding dynamic-component parameters should not ripple past this boundary.

Use v1.3.1's dynamic component support deliberately. Dynamic components (typically a URL button whose path segment changes per message, but which provider-side is one approved template) mean you can reuse a single approved template across many links/orders without re-approval. Treat the dynamic parameter as untrusted input: it lands inside a URL the user clicks, so validate it against an allow-list or a strict slug/ID pattern in the wrapper, not at the caller. Never interpolate raw user text into a dynamic URL button.

Queue everything. Sending is outbound HTTP to a third party; it will be slow and it will fail. Put it behind a job with a bounded retry policy:

final class SendWhatsAppTemplate implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 60, 300]; // provider blips, not outages

    public function handle(WhatsAppSender $sender): void
    {
        $sender->sendTemplate($this->to, $this->template, $this->params, $this->dynamic);
    }

    public function retryUntil(): DateTime { return now()->addHours(6); }
}

Three retries with escalating backoff covers transient 5xx and rate-limit responses. Do not retry on 4xx — a malformed number, an unapproved template, or a dynamic parameter that doesn't match the approved component will never succeed, and you'll just burn through your rate limit and your logs.

Bridge into Laravel Notifications so WhatsApp sits alongside mail and SMS in your existing fan-out logic:

final class InvoicePaid extends Notification
{
    public function via($notifiable): array { return ['mail', 'whatsapp']; }

    public function toWhatsApp($notifiable): array
    {
        return [
            'template' => 'invoice_paid',
            'params'   => [$this->invoice->number],
            'dynamic'  => ['url' => $this->invoice->publicUrl()],
        ];
    }
}

Register a custom NotificationChannel that resolves toWhatsApp() and delegates to your WhatsAppSender. Ten lines, and it means WhatsApp becomes a transport concern rather than something every feature has to know about.

Persist the provider message ID. Almost every useful follow-up — delivery receipts, read receipts, failure diagnosis — arrives via webhook keyed on the ID the send returned. Store it on your own table (whatsapp_messages: id, provider_message_id, to, template, status, sent_at, payload, and — new — dynamic_params so you can audit what was actually placed in the URL button) at send time, inside the same transaction as whatever business event triggered it. Without this you are blind: you'll know you called send() and nothing else.

Keep templates out of code. Template names and parameter counts are owned by the provider console and change without a deploy on your side. The dynamic-component release makes this sharper: the number and shape of dynamic parameters must match what was approved. Put templates (and whether they take dynamic components) in a config map or a small DB table with a validation step, so an unapproved/renamed template or a dynamic-parameter mismatch surfaces as a config error rather than a runtime 400 deep in a queued job.

Testing. The send path is HTTP, so Http::fake() on the underlying client is the cleanest seam — check whether the driver uses Laravel's HTTP client or Guzzle directly (Guzzle means you need a mock handler instead). If the package ships a facade, WhatsApp::shouldReceive(...) works too, but faking at the HTTP layer tests more of the driver for the same effort. Add a specific test for the dynamic component: assert the outgoing payload carries the variable in the component object, not at the body level — that shape is the new code path in v1.3.1 and the most likely place for a regression.

Gotchas and Tips

Maintenance risk, not abandonment, is now the headline. v1.3.1 shows the package is alive, but the release cadence is slow, the maintainer is single, and WhatsApp's Cloud API has moved on materially. Expect to fork or vendor eventually — not because the package is dead, but because provider-side changes (new template component types, new error codes, new auth header) will outpace patch releases. Mitigations, in order of preference: (1) fork into your own namespace and own future updates (the dynamic-component commit is small and shows how the driver layer is meant to grow), (2) skip it and write ~150 lines of Http::withToken(...)->post(...) against the Cloud API, (3) use it as a reference while you build (2). Even with v1.3.1, (2) remains genuinely viable for this problem domain.

Read the v1.3.1 diff before trusting "dynamic component." Changelog lines this terse almost always understate the change. Look at the actual commit to confirm: is it a new builder method (->dynamic(...)), a new key in the parameter array, or a template-level switch? The answer dictates whether existing send call sites are untouched (good) or whether the driver signature changed in a way that could break if you were passing a multi-component template array before (bad). It's a patch bump so a hard break is unlikely, but pin the version until you've checked.

The 24-hour window is not a package feature. Free-form messages only reach users who messaged you in the last 24 hours; outside that, only approved templates go through. If your first integration attempt fails with a policy error, it's almost certainly this, not the library. Design your flows template-first and treat free-form replies as a bonus when a conversation is already open.

Config caching bites. config('whatsapp') is read at boot. After changing env values: php artisan config:cache (and config:clear when debugging why a credential isn't taking). Also confirm the provider registered its config merge correctly — small packages sometimes ship a config file the service provider never merges, in which case you're reading null and blaming your env.

Phone number formatting. Providers are inconsistent about +, leading zeros, and whatsapp: prefixes. Normalise to E.164 with country code at your boundary, strip the + if the driver expects it, and validate the shape before queueing the job. A malformed number that reaches the API is a wasted retry cycle.

Failure signalling is ambiguous. Small packages often return the raw provider response instead of throwing. Check the driver source: if it returns an array/object on both success and failure, you need $response['error'] ?? null style checks — and queued jobs that silently "succeed" on a failed send are the nastiest bug class here. Consider wrapping the call so any error key throws, then let the queue handle it.

Webhooks are yours to build. Delivery/read status and inbound messages arrive at a URL you register with the provider. Expect to implement the hub.challenge GET verification yourself and to validate the X-Hub-Signature-256 HMAC on POSTs — do not skip signature validation, the endpoint is public. Exempt the route from CSRF, and respond 200 fast, then process the payload in a job; providers retry aggressively on timeouts and you'll get duplicates.

Idempotency. Between queue retries, provider retries, and webhook redelivery, duplicate messages happen. Deduplicate on (provider_message_id) for inbound status events and on a business key (e.g. invoice_id + template + dynamic parameter) for sends. The dynamic parameter matters here: the same template to the same number with a different dynamic URL is a legitimately different message, so the dedupe key must include it.

Rate limits and error codes are provider-side. A 429 needs backoff, not a retry storm; a 131026-family error means the recipient can't receive the message; a template error means the name/params don't match what was approved. With dynamic components, a 132000-family "number of parameters does not match" error is the classic sign your dynamic component count or placement is wrong. Map the handful of codes you actually see to typed exceptions rather than string-matching error messages.

Extension point. If the config exposes a driver map and the provider binds a manager, you can register a custom driver for an unsupported provider without forking. Look for an extend()/driver() method on the manager in the provider source. If there isn't one, don't fight it — the facade is a thin wrapper and bypassing it for one provider is cheaper than patching. The fact that v1.3.1 extended the template layer rather than adding a new driver is a decent sign the abstraction is being exercised where it matters most.

Never log the full request. Auth tokens, recipient numbers, and the dynamic URL parameters in storage/logs is a compliance problem in most jurisdictions (URLs can carry PII or session tokens). Redact in any exception handler that dumps the payload, and keep APP_DEBUG=false anywhere real numbers are in play.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource