blissjaspis/laravel-whatsapp-cloud-api 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

blissjaspis/laravel-whatsapp-cloud-api

Composer 安装命令:

composer require blissjaspis/laravel-whatsapp-cloud-api

包简介

Laravel package for interacting to whatsapp cloud api

README 文档

README

Tests Latest Version on Packagist Total Downloads

Laravel package to interact with the WhatsApp Cloud API.

  • Outbound — send messages, media, templates, mark as read, and structured Graph API errors (sendOrFail()).
  • Inbound — webhook verification, payload parsing, inbound media helpers, delivery failure details, optional idempotency (you provide routes and business logic).

Requirements

  • PHP 8.2 or higher
  • Laravel 11, 12 or 13

Installation

composer require blissjaspis/laravel-whatsapp-cloud-api:^1.2

Publish the configuration file:

php artisan vendor:publish --provider="BlissJaspis\\WhatsappCloudApi\\WhatsappServiceProvider" --tag="config"

Use Outbound to send messages, Inbound to receive webhooks, or both. Each section below includes its own configuration and examples.

Outbound

Send messages and call the Graph API.

Configuration

Each connection is one Meta app. Credentials and Graph API version sit on the connection; phone numbers are nested under phones.

WHATSAPP_DEFAULT=default
WHATSAPP_VERSION_SDK=v25.0
WHATSAPP_PHONE_NUMBER_ID=
WHATSAPP_ACCESS_TOKEN=
WHATSAPP_WEBHOOK_APP_SECRET=
WHATSAPP_WEBHOOK_VERIFY_TOKEN=
Variable Description
WHATSAPP_DEFAULT Default target: connection name or connection.phone (e.g. default.sales)
WHATSAPP_VERSION_SDK Graph API version for the default connection
WHATSAPP_PHONE_NUMBER_ID Phone number ID for the default connection (phones.main)
WHATSAPP_ACCESS_TOKEN System user access token for the default connection

Define connections in config/whatsapp-cloud-api.php:

'default' => env('WHATSAPP_DEFAULT', 'default'),

'connections' => [
    'default' => [
        'version_sdk' => env('WHATSAPP_VERSION_SDK', 'v25.0'),
        'access_token' => env('WHATSAPP_ACCESS_TOKEN'),
        'webhook_app_secret' => env('WHATSAPP_WEBHOOK_APP_SECRET'),
        'webhook_verify_token' => env('WHATSAPP_WEBHOOK_VERIFY_TOKEN'),
        'default_phone' => 'main',
        'phones' => [
            'main' => ['phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID')],
            'sales' => ['phone_number_id' => env('WHATSAPP_SALES_PHONE_NUMBER_ID')],
        ],
    ],
    'brand_b' => [
        'version_sdk' => env('WHATSAPP_BRAND_B_VERSION_SDK', 'v25.0'),
        'access_token' => env('WHATSAPP_BRAND_B_ACCESS_TOKEN'),
        'webhook_app_secret' => env('WHATSAPP_BRAND_B_WEBHOOK_APP_SECRET'),
        'phones' => [
            'support' => ['phone_number_id' => env('WHATSAPP_BRAND_B_PHONE_NUMBER_ID')],
        ],
    ],
],

Send from a connection or a specific phone:

Whatsapp::connection('default.sales')
    ->message()
    ->to('6281234567890')
    ->body(Text::message('Hello from sales')->build())
    ->send();

Whatsapp::connection('brand_b')->media()->upload($path, Media::ImageJPEG);
Whatsapp::connection('default.sales')->readMessage('wamid.xxx');

Whatsapp::message() without connection() uses the target named in WHATSAPP_DEFAULT (default: default).

Validate configuration

List connections and spot missing tokens or phone number IDs:

php artisan whatsapp:connections
php artisan whatsapp:connections --validate   # exit code 1 when invalid

Optional boot-time checks (reports to logs, does not stop the app):

WHATSAPP_VALIDATE_ON_BOOT=true

Core pattern

All outbound messages follow the same pattern:

Whatsapp::message()
    ->to($phoneNumber)
    ->body($messageBuilder->build())
    ->send(); // Illuminate\Http\Client\Response

Use ->sendOrFail() instead of ->send() when you want a WhatsappApiException on Graph API errors (see Graph API errors).

Use the Whatsapp facade or resolve BlissJaspis\WhatsappCloudApi\Whatsapp from the container.

Graph API errors

Failed requests return a normal Response from send(). For structured Meta error fields, use sendOrFail() or WhatsappApiException::throwIfFailed():

use BlissJaspis\WhatsappCloudApi\Exceptions\WhatsappApiException;
use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;
use BlissJaspis\WhatsappCloudApi\Support\Text;

try {
    Whatsapp::connection('default')
        ->message()
        ->to('6281234567890')
        ->body(Text::message('Hello')->build())
        ->sendOrFail();
} catch (WhatsappApiException $e) {
    // $e->getMessage()
    // $e->getCode() — HTTP status (400, 401, …)
    // $e->apiCode, $e->errorSubcode, $e->type, $e->fbtraceId, $e->error
}

$response = Whatsapp::message()->to('628...')->body(...)->send();
WhatsappApiException::throwIfFailed($response);

Media and read receipts use the same *OrFail() helpers:

use BlissJaspis\WhatsappCloudApi\Enums\Media;
use BlissJaspis\WhatsappCloudApi\WhatsappRead;

$upload = Whatsapp::connection('default')->media()->uploadOrFail('/path/to/file.jpg', Media::ImageJPEG);

(new WhatsappRead('default.sales'))->sendOrFail('wamid.xxx');

Phone numbers

The package passes the recipient number to the API as-is. Format the number in your application before calling to(). WhatsApp expects digits only with the country code included and no + prefix (e.g. 6281234567890).

Text messages

use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;
use BlissJaspis\WhatsappCloudApi\Support\Text;

$response = Whatsapp::message()
    ->to('6281234567890')
    ->body(Text::message('Hello from Laravel!')->build())
    ->sendOrFail();

$messageId = $response->json('messages.0.id');

Disable link preview:

Text::message('Visit https://example.com')->disableLinkPreview()->build();

Replying to a message

Whatsapp::message()
    ->to('6281234567890')
    ->replyTo('wamid.xxx')
    ->body(Text::message('Thanks for your message!')->build())
    ->send();

Media messages

Media builders accept an uploaded media ID (asset) or a public HTTPS URL (url).

Image

use BlissJaspis\WhatsappCloudApi\Support\Image;

// By URL
Whatsapp::message()
    ->to('6281234567890')
    ->body(Image::media('https://example.com/image.jpg', 'url')->build())
    ->send();

// By media ID with caption
Whatsapp::message()
    ->to('6281234567890')
    ->body(Image::media('your-media-id')->caption('Your caption')->build())
    ->send();

Document

use BlissJaspis\WhatsappCloudApi\Support\Document;

Whatsapp::message()
    ->to('6281234567890')
    ->body(
        Document::media('https://example.com/document.pdf', 'url')
            ->filename('invoice.pdf')
            ->caption('Your invoice')
            ->build()
    )
    ->send();

Audio

use BlissJaspis\WhatsappCloudApi\Support\Audio;

Whatsapp::message()
    ->to('6281234567890')
    ->body(Audio::media('your-media-id')->build())
    ->send();

Video

use BlissJaspis\WhatsappCloudApi\Support\Video;

Whatsapp::message()
    ->to('6281234567890')
    ->body(Video::media('your-media-id')->caption('Optional caption')->build())
    ->send();

Sticker

use BlissJaspis\WhatsappCloudApi\Support\Sticker;

Whatsapp::message()
    ->to('6281234567890')
    ->body(Sticker::media('your-media-id')->build())
    ->send();

Template messages

use BlissJaspis\WhatsappCloudApi\Support\Template;

// Static template (no variables)
Whatsapp::message()
    ->to('6281234567890')
    ->body(Template::name('hello_world')->lang('en')->build())
    ->send();

// Template with variables (fluent components)
use BlissJaspis\WhatsappCloudApi\Support\Template\BodyComponent;
use BlissJaspis\WhatsappCloudApi\Support\Template\ButtonComponent;
use BlissJaspis\WhatsappCloudApi\Support\Template\HeaderComponent;

Whatsapp::message()
    ->to('6281234567890')
    ->body(
        Template::name('order_update')
            ->lang('en')
            ->components([
                HeaderComponent::text('Your order'),
                BodyComponent::make()->text('John')->text('#12345'),
                ButtonComponent::url(0, 'https://example.com/orders/12345'),
            ])
            ->build()
    )
    ->send();

BodyComponent also supports currency() and dateTime() for non-text variables. Use raw components([...]) arrays for uncommon template layouts.

Optional category when your WhatsApp Business setup requires it:

Template::name('promo_offer')->lang('en')->category('marketing')->build();

Location

use BlissJaspis\WhatsappCloudApi\Support\Location;

Whatsapp::message()
    ->to('6281234567890')
    ->body(
        Location::mark()
            ->latitude(-6.2088)
            ->longitude(106.8456)
            ->name('Jakarta')
            ->address('Indonesia')
            ->build()
    )
    ->send();

Location request

use BlissJaspis\WhatsappCloudApi\Support\LocationRequest;

Whatsapp::message()
    ->to('6281234567890')
    ->body(LocationRequest::message('Please share your location.')->build())
    ->send();

Interactive messages

List message with fluent action helpers:

use BlissJaspis\WhatsappCloudApi\Support\Interactive;
use BlissJaspis\WhatsappCloudApi\Support\Interactive\ListAction;
use BlissJaspis\WhatsappCloudApi\Support\Interactive\ReplyButtonAction;

Whatsapp::message()
    ->to('6281234567890')
    ->body(
        Interactive::list()
            ->body('Choose an option')
            ->header('Menu')
            ->action(
                ListAction::make('View options')
                    ->section('Section 1')
                    ->row('row_1', 'Option 1', 'First option')
            )
            ->build()
    )
    ->send();

// Reply buttons (up to 3)
Whatsapp::message()
    ->to('6281234567890')
    ->body(
        Interactive::button()
            ->body('Do you agree?')
            ->action(
                ReplyButtonAction::make()
                    ->reply('yes', 'Yes')
                    ->reply('no', 'No')
            )
            ->build()
    )
    ->send();

Fluent entry points for other interactive types (pass a raw action array for the payload):

Interactive::flow()->body('Complete the form')->action([/* flow action */])->build();
Interactive::productList()->body('Browse')->action([/* catalog */])->build();
Interactive::catalog()->body('View catalog')->action([/* catalog_message */])->build();

Contacts and reactions

use BlissJaspis\WhatsappCloudApi\Support\Contacts;
use BlissJaspis\WhatsappCloudApi\Support\Reaction;

Whatsapp::message()
    ->to('6281234567890')
    ->body(Contacts::data([/* WhatsApp contacts array */])->build())
    ->send();

Whatsapp::message()
    ->to('6281234567890')
    ->body(Reaction::messageId('wamid.xxx')->emoji('👍')->build())
    ->send();

Media upload, retrieve, and delete

use BlissJaspis\WhatsappCloudApi\Enums\Media;
use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;
use BlissJaspis\WhatsappCloudApi\Support\Image;

$upload = Whatsapp::media()->upload('/path/to/image.jpg', Media::ImageJPEG);
$mediaId = $upload->json('id');

Whatsapp::message()
    ->to('6281234567890')
    ->body(Image::media($mediaId)->build())
    ->send();

$metadata = Whatsapp::media()->retrieve($mediaId);
$binary = Whatsapp::media()->download($metadata->json('url'));
Whatsapp::media()->delete($mediaId);

// Or throw WhatsappApiException on failure:
$metadata = Whatsapp::media()->retrieveOrFail($mediaId);
$binary = Whatsapp::media()->downloadOrFail($metadata->json('url'));

Mark a message as read

use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;

// Uses WHATSAPP_DEFAULT connection
Whatsapp::readMessage('wamid.xxx');
Whatsapp::readMessage('wamid.xxx', withTypingIndicator: true);

// Named connection with sendOrFail()
use BlissJaspis\WhatsappCloudApi\WhatsappRead;

(new WhatsappRead('default.sales'))->sendOrFail('wamid.xxx');

Inbound

Receive WhatsApp webhooks in your Laravel app. You provide routes, controllers, jobs, and business logic; this package supplies verification and optional idempotency helpers.

Configuration

Webhook credentials are set per Meta app under connections (see Outbound configuration):

'connections' => [
    'default' => [
        'webhook_app_secret' => env('WHATSAPP_WEBHOOK_APP_SECRET'),
        'webhook_verify_token' => env('WHATSAPP_WEBHOOK_VERIFY_TOKEN'),
        // ...
    ],
],
Config key Description
connections.*.webhook_app_secret App Secret (Meta Developer Console → App settings → Basic)
connections.*.webhook_verify_token Same token as in your Meta webhook subscription
webhook_idempotency Optional: prefix, ttl, cache_store (see below)

Subscription challenge (GET)

When you configure the webhook URL in Meta, respond to the verification request:

use BlissJaspis\WhatsappCloudApi\Webhooks\WebhookChallenge;
use Illuminate\Http\Request;

Route::get('/webhook/whatsapp', fn (Request $request) => WebhookChallenge::toResponse($request));

Signature verification (POST)

Verify X-Hub-Signature-256 using the raw request body and your app secret:

use BlissJaspis\WhatsappCloudApi\Http\Middleware\VerifyWhatsappWebhookSignature;

Route::post('/webhook/whatsapp', function (Request $request) {
    // Handle payload in your app
})->middleware(VerifyWhatsappWebhookSignature::class);

When no middleware parameter is passed, the app secret is read from the default connection (WHATSAPP_DEFAULT, usually default).

Pass a connection name (from connections) or a literal app secret after ::

Route::post('/webhook/whatsapp', function (Request $request) {
    // Handle payload in your app
})->middleware(VerifyWhatsappWebhookSignature::class . ':your-meta-app-secret');

// Connection name (per Meta app — not connection.phone)
Route::post('/webhook/whatsapp/support', $handler)
    ->middleware(VerifyWhatsappWebhookSignature::class . ':support');

Or verify manually:

use BlissJaspis\WhatsappCloudApi\Webhooks\WebhookSignature;

WebhookSignature::assertValidRequest($request); // uses config; throws InvalidWebhookSignature
WebhookSignature::assertValidRequest($request, 'custom-app-secret');

Note: Signature verification uses the raw request body. Do not parse the JSON before the middleware runs. If your app secret contains :, Laravel splits middleware parameters on that character — use config only or WebhookSignature::verifyRequest($request, $secret) instead.

Idempotency (optional)

Meta may retry webhooks. Use WebhookPayload to extract message/status IDs and skip duplicates:

use BlissJaspis\WhatsappCloudApi\Contracts\WebhookIdempotencyStore;
use BlissJaspis\WhatsappCloudApi\Http\Middleware\EnsureWhatsappWebhookIdempotency;
use BlissJaspis\WhatsappCloudApi\Webhooks\WebhookPayload;

Route::post('/webhook/whatsapp', function (Request $request) {
    // ...
})->middleware([
    VerifyWhatsappWebhookSignature::class,
    EnsureWhatsappWebhookIdempotency::class,
]);

Configure webhook_idempotency in config/whatsapp-cloud-api.php (default TTL: 24 hours). Bind your own WebhookIdempotencyStore for database-backed deduplication. For production, use Redis via WHATSAPP_WEBHOOK_IDEMPOTENCY_CACHE_STORE.

Payload parsing

Parse nested Meta payloads into typed events instead of drilling into entry[0]['changes'][0] manually:

use BlissJaspis\WhatsappCloudApi\Webhooks\Events\IncomingMessage;
use BlissJaspis\WhatsappCloudApi\Webhooks\Events\MessageStatus;
use BlissJaspis\WhatsappCloudApi\Webhooks\WebhookPayload;

Route::post('/webhook/whatsapp', function (Request $request) {
    foreach (WebhookPayload::events($request->all()) as $event) {
        if ($event instanceof MessageStatus) {
            // delivery/read receipts
            continue;
        }

        if (! $event instanceof IncomingMessage || ! $event->isMessagesField()) {
            continue;
        }

        if ($reply = $event->interactiveReply()) {
            ProcessInteractiveResponseJob::dispatch($event->raw(), $reply->id);

            continue;
        }

        if ($text = $event->textBody()) {
            // handle plain text
        }

        if ($mediaId = $event->mediaId()) {
            // image, video, audio, document, or sticker — see inbound media below
        }
    }
})->middleware(VerifyWhatsappWebhookSignature::class);

Helpers:

Method Returns
WebhookPayload::events() All parsed events (IncomingMessage, MessageStatus, MessageTemplateStatusUpdate, UnknownWebhookEvent)
WebhookPayload::incomingMessages() Incoming messages only
WebhookPayload::messageStatuses() Delivery/read/failed statuses only
WebhookPayload::messageTemplateStatusUpdates() Template approval/rejection updates from Meta

IncomingMessage

Method Description
textBody(), hasTextBody() Plain text messages
interactiveReply(), buttonReplyId(), listReplyId() Button/list/NFM replies
mediaId(), mimeType(), sha256(), mediaCaption(), documentFilename() Inbound image/video/audio/document/sticker
location(), isLocation() Shared location coordinates and label
from(), phoneNumberId(), messageType(), raw() Routing and debugging

Unknown webhook fields are returned as UnknownWebhookEvent so you can still read value() without the parser failing.

MessageStatus and delivery failures

foreach (WebhookPayload::messageStatuses($request->all()) as $status) {
    if ($status->isFailed() && ($error = $status->firstError())) {
        // $error->code, $error->title, $error->message, $error->details
    }

    if ($status->isDelivered()) {
        // ...
    }
}

errors() returns a list of WhatsappDeliveryError value objects parsed from Meta's statuses[].errors payload.

Template status updates

use BlissJaspis\WhatsappCloudApi\Webhooks\Events\MessageTemplateStatusUpdate;

foreach (WebhookPayload::messageTemplateStatusUpdates($request->all()) as $event) {
    if ($event->isApproved()) {
        // $event->templateName(), $event->templateLanguage()
    }
}

Download inbound media

When a user sends media, use mediaId() with the same connection that received the message:

use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;
use BlissJaspis\WhatsappCloudApi\Webhooks\Events\IncomingMessage;

if ($event instanceof IncomingMessage && ($mediaId = $event->mediaId())) {
    $meta = Whatsapp::connection($connection->name)->media()->retrieveOrFail($mediaId);
    $file = Whatsapp::connection($connection->name)->media()->downloadOrFail($meta->json('url'));

    // $event->mimeType(), $event->sha256(), $event->mediaCaption()
}

Route inbound messages to a connection (multiple numbers)

One webhook URL can receive events from every number in the same Meta app. Use phoneNumberId() to pick the connection, then reply from that number:

use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;
use BlissJaspis\WhatsappCloudApi\Webhooks\Events\IncomingMessage;
use BlissJaspis\WhatsappCloudApi\WhatsappConnectionManager;

foreach (WebhookPayload::events($request->all()) as $event) {
    if (! $event instanceof IncomingMessage) {
        continue;
    }

    $connection = app(WhatsappConnectionManager::class)
        ->findByPhoneNumberId($event->phoneNumberId() ?? '');

    if ($connection === null) {
        continue;
    }

    Whatsapp::connection($connection->name)
        ->message()
        ->to($event->from() ?? '')
        ->body(Text::message('Thanks!')->build())
        ->send();
}

HTTP observability

Before each Graph API request, the package dispatches BlissJaspis\WhatsappCloudApi\Events\HttpSending with the pending HTTP client and resolved WhatsappConnection. Listen in your app to log URLs, connection names, or add headers:

use BlissJaspis\WhatsappCloudApi\Events\HttpSending;

Event::listen(HttpSending::class, function (HttpSending $event) {
    logger()->info('WhatsApp API request', [
        'connection' => $event->connection->name,
        'phone_number_id' => $event->connection->phoneNumberId,
    ]);
});

Queue outbound messages

The package does not queue messages automatically. Dispatch a job from your app:

use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;

SendWhatsappTextJob::dispatch('6281234567890', 'Hello');

// Job example:
final class SendWhatsappTextJob implements ShouldQueue
{
    public function __construct(
        private string $to,
        private string $body,
        private ?string $connection = null,
    ) {}

    public function handle(): void
    {
        $whatsapp = $this->connection
            ? Whatsapp::connection($this->connection)
            : Whatsapp::message();

        $whatsapp->to($this->to)
            ->body(['type' => 'text', 'text' => ['body' => $this->body]])
            ->sendOrFail();
    }
}

Testing

Fake outbound calls without hitting Meta (requires phpunit/phpunit in your app for assertion helpers):

use BlissJaspis\WhatsappCloudApi\Facades\Whatsapp;

Whatsapp::fake();

Whatsapp::message()
    ->to('6281234567890')
    ->body(['type' => 'text', 'text' => ['body' => 'Hello']])
    ->send();

Whatsapp::fake()->assertSentTo('6281234567890');
composer lint

Laravel Boost

This package includes a Laravel Boost agent skill at resources/boost/skills/laravel-whatsapp-cloud-api/SKILL.md to help AI agents generate correct integration code.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

blissjaspis/laravel-whatsapp-cloud-api 适用场景与选型建议

blissjaspis/laravel-whatsapp-cloud-api 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 101 次下载、GitHub Stars 达 0, 最近一次更新时间为 2024 年 05 月 16 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 blissjaspis/laravel-whatsapp-cloud-api 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 blissjaspis/laravel-whatsapp-cloud-api 我们能提供哪些服务?
定制开发 / 二次开发

基于 blissjaspis/laravel-whatsapp-cloud-api 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 101
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 4
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2024-05-16