reshapify / sendseven-laravel
Unofficial. SendSeven for Laravel: configured client, verified webhooks as Laravel events, channel onboarding redirects, a shared rate limit and a doctor command.
Requires
- php: ^8.3
- illuminate/cache: ^12.0||^13.0
- illuminate/console: ^12.0||^13.0
- illuminate/contracts: ^12.0||^13.0
- illuminate/http: ^12.0||^13.0
- illuminate/routing: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
- reshapify/sendseven: ^0.2
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-type-coverage: ^4.0
- phpstan/phpstan-strict-rules: ^2.0
- rector/rector: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Unofficial. Not affiliated with or endorsed by SendSeven GmbH or Laravel Holdings.
Laravel integration for reshapify/sendseven, the typed SDK for the SendSeven messaging API (WhatsApp, SMS, email, Telegram, Messenger, Instagram, RCS, browser push).
- Configured client: the
SendSevenfacade, or injectReshapify\SendSeven\Client. - Webhooks in one line:
Route::sendSevenWebhooks()answers SendSeven's verification challenge, verifies signatures, ignores redeliveries and dispatches typed Laravel events. - Customer onboarding:
SendSeven::connect()sends customers to SendSeven's connect page (WhatsApp Embedded Signup, Instagram, Messenger, Telegram…) and tells you when their channel arrives. - One rate limit for every process: web requests, queue workers and the scheduler share the token's 100-requests-a-minute budget through your cache.
php artisan sendseven:doctorchecks the token, the plan, tenancy and the webhook endpoint against the live API.- Fakes for the API and signed webhook deliveries.
Install
composer require reshapify/sendseven-laravel
php artisan vendor:publish --tag=sendseven-config # optional
Composer may ask whether to trust php-http/discovery, a plugin of the core SDK. Laravel already ships an HTTP client, so answering "no" is fine.
SENDSEVEN_API_TOKEN=s7_api_...
Locally, SENDSEVEN_DRIVER=log sends nothing and needs no token: requests are written to the log and answered with a placeholder, like Laravel's log mail driver.
Send
use Reshapify\SendSeven\Laravel\Facades\SendSeven; SendSeven::messages()->send(to: '+4915112345678', channelId: $channelId, text: 'Your order has shipped.');
To send to a phone number, look its contact up once; the ID is cached for an hour (contacts.cache_for_seconds):
$contactId = SendSeven::contactIdForPhone('+4915112345678'); SendSeven::messages()->sendText(channelId: $channelId, text: 'Your order has shipped.', contactId: $contactId);
For messages someone is waiting on, such as a login code, SendSeven::fast() is the same client with a 10-second timeout and no retries, so the app can fall back to email quickly.
Every SendSeven endpoint is available; see the SDK docs. For a customer's own token use SendSeven::forToken($token); for a sub-account with a partner token use SendSeven::forTenant($tenantId).
Receive webhooks
// routes/web.php (CSRF is skipped for this route; deliveries are signed instead) Route::sendSevenWebhooks(); // POST /webhooks/sendseven, named sendseven.webhooks
php artisan sendseven:webhooks:register # creates the endpoint and prints SENDSEVEN_WEBHOOK_SECRET php artisan sendseven:doctor # confirms SendSeven verified it
Listen for the SDK's own event classes:
use Reshapify\SendSeven\Webhooks\Events\MessageReceived; final class StoreInboundMessage implements ShouldQueue { public function handle(MessageReceived $event): void { Inbox::store(from: $event->message->fromId, text: $event->message->text); } }
| Event | When |
|---|---|
Reshapify\SendSeven\Webhooks\Events\MessageReceived |
Someone messaged you |
…\MessageStatusUpdated |
Sent, delivered, read or failed (failed(), message->errorCode()) |
…\MessageReactionChanged, ChannelEvent, ContactEvent, ConversationEvent, UnknownEvent |
The rest |
Reshapify\SendSeven\Laravel\Events\ChannelConnected |
A channel was created, e.g. through a connect link |
Reshapify\SendSeven\Laravel\Events\WebhookReceived |
Every delivery, with the route parameters |
Each event ID is handled once (configurable: webhooks.deduplicate_for_seconds). If a listener throws, the event is forgotten and SendSeven gets an error, so its redelivery is handled rather than skipped. Listeners run during the request and SendSeven waits for the answer, so queue slow work.
An endpoint per customer? Put a parameter in the URL and resolve its secret:
Route::sendSevenWebhooks('webhooks/sendseven/{workspace}'); // AppServiceProvider::boot() SendSeven::resolveWebhookSecretUsing( fn (Request $request) => Workspace::find($request->route('workspace'))?->sendseven_webhook_secret, );
Returning null answers 404. To use your own controller instead, add the Reshapify\SendSeven\Laravel\Webhooks\VerifyWebhookSignature middleware and read the event with VerifyWebhookSignature::event($request).
Let customers connect their channels
use Reshapify\SendSeven\Enums\ChannelType; use Reshapify\SendSeven\Onboarding\ConnectLink; use Reshapify\SendSeven\Onboarding\WhatsAppMode; public function store(Workspace $workspace) { $connection = SendSeven::connect( ConnectLink::for(ChannelType::WhatsApp) ->modes(whatsapp: [WhatsAppMode::Classic]) ->brandedAs(config('app.name')) ->redirectTo(route('channels.index')) ->singleUse(), ); $workspace->update(['sendseven_connect_link_id' => $connection->link->id]); return $connection; // a redirect; Inertia requests get a full-page visit }
public function handle(ChannelConnected $event): void { Workspace::firstWhere('sendseven_connect_link_id', $event->connectLinkId())?->channels()->create([...]); }
SendSeven runs Meta's Embedded Signup on its connect page; the SDK's onboarding guide covers modes, delegation and what customers see.
Rate limits
SendSeven allows 100 standard requests a minute per token. The add-on counts every request in your cache (set SENDSEVEN_RATE_LIMIT_STORE to a store all processes share, like redis), keeps 10% headroom, and pauses everyone when SendSeven answers 429. When the minute's budget is spent it throws RateLimitExceeded instead of sending.
In queued jobs, the ReleaseWhenRateLimited middleware puts the job back on the queue until there is capacity, whether the shared budget ran out or SendSeven answered 429:
use Reshapify\SendSeven\Laravel\Queue\ReleaseWhenRateLimited; public function middleware(): array { return [new ReleaseWhenRateLimited]; }
Releases count as attempts, so give such jobs room with $tries or retryUntil().
With a partner token, rate_limit.tenant_share stops one busy tenant from starving the others.
Testing
SendSeven traffic goes through Laravel's HTTP client, so Http::fake(), Http::preventStrayRequests() and Http::assertSent() already cover it. Set SENDSEVEN_MAX_ATTEMPTS=1 in phpunit.xml so faked 5xx responses aren't retried.
Or script SendSeven's responses by endpoint:
$fake = SendSeven::fake([ 'POST /messages' => ['id' => 'msg_1', 'direction' => 'outbound', 'message_type' => 'text', 'status' => 'queued', 'created_at' => '2026-10-01T09:00:00Z'], ]); $this->post('/orders/1/ship'); $fake->assertSent('POST /messages', fn ($request) => $request->body['to'] === '+4915112345678');
SendSeven::fake() also replaces SendSeven::fast().
Webhooks, signed as SendSeven signs them:
use Reshapify\SendSeven\Laravel\Testing\InteractsWithSendSevenWebhooks; $this->postSendSevenWebhook('/webhooks/sendseven', $this->sendSevenWebhookPayload('message.received', [ 'message' => ['id' => 'msg_1', 'direction' => 'inbound', 'message_type' => 'text', 'text' => 'Hi', 'from_id' => '+4915112345678'], ]))->assertOk();
Commands
| Command | Does |
|---|---|
sendseven:doctor |
Checks the token, plan, tenancy, webhook secret and endpoint, and the rate-limit store |
sendseven:sms-prices {country?} |
SMS prices per segment |
Webhook endpoints. Leave out id to act on the endpoint behind Route::sendSevenWebhooks(), or on the only one there is.
| Command | When you need it |
|---|---|
sendseven:webhooks:register {url?} |
Setting up: creates the endpoint, prints its secret once, confirms verification |
sendseven:webhooks:list |
Every endpoint with its state (active, suspended, unverified, inactive) and last error |
sendseven:webhooks:show {id?} |
One endpoint in detail |
sendseven:webhooks:deliveries {id?} {--status=failed} |
Messages aren't arriving: what SendSeven sent, and what your app answered |
sendseven:webhooks:retry {delivery} {id?} |
Re-send a delivery your app missed |
sendseven:webhooks:test {id?} |
Check the app accepts a delivery end to end |
sendseven:webhooks:activate {id?} |
After an outage: SendSeven suspends endpoints after repeated failures |
sendseven:webhooks:update {id?} {--url=} {--event=*} {--name=} |
The URL or events changed |
sendseven:webhooks:rotate {id?} |
A secret leaked: new secret, printed once (asks first) |
sendseven:webhooks:delete {id?} |
Remove an endpoint (asks first) |
For AI agents
With Laravel Boost, php artisan boost:install picks up this package's guidelines and its sendseven-development skill. Without it, point your agent at vendor/reshapify/sendseven-laravel/resources/boost/skills/sendseven-development/SKILL.md.
Security
Report vulnerabilities privately; see SECURITY.md.
License
MIT. Unofficial: not affiliated with or endorsed by SendSeven GmbH or Laravel Holdings. "SendSeven" and "Laravel" are trademarks of their owners and are used only to say what this package works with.