reshapify / sendseven
Unofficial. A typed, fully documented PHP SDK for the SendSeven messaging API: WhatsApp, SMS, email, Telegram, Messenger, Instagram and more.
Requires
- php: ^8.3
- php-http/discovery: ^1.20
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.1
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.9
- laravel/pint: ^1.18
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-type-coverage: ^3.5|^4.0
- phpstan/phpstan: ^2.1
- 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.
A typed PHP SDK for the SendSeven messaging API: WhatsApp, SMS, email, Telegram, Messenger, Instagram, RCS and browser push, from one client.
- Every endpoint. All 712 operations, generated from SendSeven's OpenAPI spec and corrected where the live API differs.
- Typed throughout, and forgiving. Responses are readonly objects with real types; lists paginate lazily; enums stay open to new values; a field SendSeven leaves out reads as empty instead of breaking the call.
- Webhooks done right. The activation challenge, timestamped signatures and typed events, verified against live deliveries.
- Safe by default. Idempotency keys on every write, retries only where safe, and errors that say how to fix them.
- Built for tests.
SendSeven::fake()scripts responses and asserts requests through the real pipeline.
composer require reshapify/sendseven
Requires PHP 8.3+ and any PSR-18 HTTP client (Guzzle, Symfony HttpClient…), which is found automatically. Laravel? Use reshapify/sendseven-laravel.
Send a message
use Reshapify\SendSeven\SendSeven; $sendseven = SendSeven::client(getenv('SENDSEVEN_API_TOKEN')); $message = $sendseven->messages()->send( to: '+4915112345678', channelId: 'ch_whatsapp', text: 'Your order has shipped.', ); echo $message->id;
Every area of the API is a method on the client: contacts(), channels(), whatsAppTemplates(), conversations(), campaigns(), webhooks() and 65 more. Arguments are named and typed; optional ones are left out of the request.
Lists
// One page $page = $sendseven->contacts()->list(search: 'acme', pageSize: 50); foreach ($page as $contact) { echo $contact->name; } // Every contact, fetching pages only as they're reached foreach ($sendseven->contacts()->list()->lazy() as $contact) { // ... }
Receive webhooks
use Reshapify\SendSeven\Webhooks\Webhook; use Reshapify\SendSeven\Webhooks\Events\MessageReceived; use Reshapify\SendSeven\Webhooks\Events\MessageStatusUpdated; $body = file_get_contents('php://input'); // the raw body, exactly as received $headers = getallheaders(); // SendSeven activates an endpoint only after this unsigned challenge is echoed. if (Webhook::isVerificationChallenge($body, $headers)) { header('Content-Type: application/json'); echo json_encode(Webhook::challengeResponse($body)); return; } $event = Webhook::constructEvent($body, $headers, $secret); // throws InvalidSignature if ($event instanceof MessageReceived) { $from = $event->message->fromId; // phone, or the channel's ID for the person $text = $event->message->text; $button = $event->message->buttonReply()?->id; // a tapped button, however it arrived } if ($event instanceof MessageStatusUpdated && $event->failed()) { $why = $event->message->errorCode(); // e.g. "131047": outside WhatsApp's 24 hours }
See the webhooks guide for every event and how SendSeven signs them.
Let customers connect their own channels
SendSeven runs WhatsApp's Embedded Signup (and Messenger, Instagram, Telegram, SMS and email onboarding) on a hosted page. Create a link, send the customer to it, and a channel.created webhook tells you when they're done.
use Reshapify\SendSeven\Enums\ChannelType; use Reshapify\SendSeven\Onboarding\ConnectLink; use Reshapify\SendSeven\Onboarding\WhatsAppMode; $link = $sendseven->connectLinks()->create( ConnectLink::for(ChannelType::WhatsApp) ->modes(whatsapp: [WhatsAppMode::Classic]) ->brandedAs('Acme') ->redirectTo('https://app.acme.test/channels/connected') ->singleUse() ->expiresIn(hours: 48), ); header('Location: '.$link->connectUrl);
More in onboarding channels.
One tenant, or many
A token belongs to one SendSeven tenant (workspace), and everything works with it on any plan that includes the API. Platforms that give each customer their own sub-account can act on it with forTenant():
$sendseven->forTenant($customerTenantId)->contacts()->list();
That needs SendSeven's multi-tenant management (Professional, Scale, Enterprise or API Only; not Basic, or a trial that has ended), and creating tenants needs a token from the billing account's owner. Check before you rely on it:
$capabilities = $sendseven->capabilities(); if (! $capabilities->canCreateTenants()) { echo $capabilities->whyNotCreateTenants(); }
See tenancy and plans.
Errors
Every exception implements Reshapify\SendSeven\Exceptions\SendSevenException, and its message names the endpoint, the status and what to do:
feature_disabled (POST /tenants, HTTP 403, request req_8f2…). The account's SendSeven plan doesn't
include multi_tenant: it comes with Professional, Scale, Enterprise or API Only.
| Exception | When |
|---|---|
AuthenticationFailed |
401: the token is wrong, expired or revoked |
PermissionDenied |
403: the token lacks a scope |
FeatureDisabled |
403: the plan lacks a feature; feature() names it |
NotBillingAccountOwner |
403: only the billing account's owner can create tenants |
NotFound |
404 |
ValidationFailed |
422; fieldErrors() lists each field |
RateLimited |
429 after retries; retryAfter() |
InsufficientBalance |
402 or an insufficient_* code, e.g. the RCS wallet |
ServerError |
5xx after retries |
TransportFailed |
SendSeven couldn't be reached |
UnexpectedResponse |
a field came back with the wrong type, or the body wasn't JSON. A field that's simply missing reads as an empty value instead ('', 0, false, []), so the original is on ->raw() |
Retries, rate limits and idempotency
Writes get an Idempotency-Key automatically, so retrying them is safe; pass your own (idempotencyKey: 'order-42-shipped') to make a repeat from your side safe too. 429s, 5xx and connection failures are retried up to three times with backoff, honouring Retry-After. A token allows 100 standard requests a minute: give processes that share it a shared RateLimiter. See resilience.
$sendseven = SendSeven::factory() ->withToken($token) ->withRetries(5) ->withRateLimiter($sharedLimiter) ->withHttpClient($psr18Client) ->make();
Testing
$fake = SendSeven::fake([ 'POST /messages' => ['id' => 'msg_1', 'direction' => 'outbound', 'message_type' => 'text', /* ... */], ]); $service = new OrderNotifier($fake->client); $service->shipped($order); $fake->assertSent('POST /messages', fn ($request) => $request->body['to'] === '+4915112345678');
Webhook tests can sign their own deliveries with Webhook::sign($body, $secret). See testing.
Anything else
$sendseven->request(Method::Get, '/some/new/endpoint') calls any endpoint with the same authentication, retries and errors. ->raw() on any response object returns the original JSON, including fields the SDK doesn't model.
Documentation
- API reference: every endpoint, its parameters, scopes and return type
- Guides: webhooks, onboarding channels, tenancy, channels, resilience, testing
- Known quirks: where SendSeven differs from its spec
- For AI agents: llms.txt, AGENTS.md, and a skill
Contributing
See CONTRIBUTING.md. Report vulnerabilities privately: SECURITY.md.
License
MIT. Unofficial: not affiliated with or endorsed by SendSeven GmbH; "SendSeven" is used only to say what this SDK works with. openapi/sendseven.json is SendSeven's published API description and isn't covered by this license; see openapi/NOTICE.md.