Search by

reshapify / sendseven-laravel

richbowen

Unofficial. SendSeven for Laravel: configured client, verified webhooks as Laravel events, channel onboarding redirects, a shared rate limit and a doctor command.

v0.2.1 2026-10-01 18:33 UTC

This package is auto-updated.

Last update: 2026-10-01 18:35:58 UTC


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 SendSeven facade, or inject Reshapify\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:doctor checks 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_...

Send

use Reshapify\SendSeven\Laravel\Facades\SendSeven;

SendSeven::messages()->send(to: '+4915112345678', channelId: $channelId, text: 'Your order has shipped.');

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).

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:

public function handle(): void
{
    try {
        SendSeven::messages()->send(...);
    } catch (RateLimitExceeded $exception) {
        $this->release($exception->retryAfter());
    }
}

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');

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.