Search by

reshapify / sendseven

richbowen

Unofficial. A typed, fully documented PHP SDK for the SendSeven messaging API: WhatsApp, SMS, email, Telegram, Messenger, Instagram and more.

v0.1.2 2026-10-01 15:06 UTC

This package is auto-updated.

Last update: 2026-10-01 15:07:48 UTC


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

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.