PHP SDK for the S-Mailer API — send messages across SMS, email, WhatsApp and more, list sent/received messages, check your balance, and verify webhooks.
Status: pre-1.0. All resource methods are implemented; the API may still change before
1.0.0.
- PHP 8.1+
- A PSR-18 HTTP client and PSR-17 factories (e.g. Guzzle). They are auto-discovered via
php-http/discovery; you can also inject your own (see Configuration).
composer require s-mailer/sdkIf you don't already have a PSR-18 client installed:
composer require guzzlehttp/guzzleEvery request authenticates with your X-Client-ID / X-Client-Secret pair. Find them in your S-Mailer dashboard.
$mailer = new \SMailer\Client('CLT-XXXXXXXX', 'your-api-secret');$result = $mailer->messages->send([
'sender' => 'sms-co1-xxxxxxxx',
'recipients' => [
'+258841234567',
['phone' => '+258842000001', 'data' => ['name' => 'Alice']],
],
'content_template' => 'Hi ${name}, your code is ${code}.',
// 'channel' => 'sms', 'provider' => 'sms-co1', // alternatives to `sender`
// 'campaign_id' => 'promo-2026',
// 'webhook_url' => 'https://your-app.com/webhooks/status',
]);
// => ['message_id' => ..., 'external_id' => ..., 'total_outbound_cost_tokens' => ...]$sent = $mailer->messages->listOutbound(['status' => 'delivered', 'channel' => 'sms', 'limit' => 50]);
$received = $mailer->messages->listInbound(['limit' => 50]);
$one = $mailer->messages->get($messageId); // outbound or inbound; see `direction`$mailer->messages->retrigger($inboundId); // unlock + re-deliver a locked (unpaid) inbound
$mailer->messages->delete($messageId); // delete an outbound or paid inbound message$tokens = $mailer->balance->get(); // int
$profile = $mailer->balance->profile(); // includes webhook_signing_key
$channels = $mailer->channels->list(); // public senders + your private ones (authenticated)For WhatsApp senders only. Text is billed at the outbound cost; media/buttons/location/contact/reactions at the advanced cost.
$sender = 'whatsapp-co1-1a2b3c4d';
$to = '+258841234567';
$mailer->whatsapp->sendText($sender, $to, 'Hello 👋');
// Host media anywhere publicly reachable, then pass the URL.
$mailer->whatsapp->sendMedia($sender, $to, [
'kind' => 'image', 'url' => 'https://cdn.example.com/promo.jpg', 'filename' => 'promo.jpg',
], caption: 'Our new catalogue');
$res = $mailer->whatsapp->sendButtons($sender, $to, 'Confirm your appointment?', [
['id' => 'yes', 'title' => 'Yes'],
['id' => 'no', 'title' => 'No'],
], footer: 'S-Mailer');
// Location pin.
$mailer->whatsapp->sendLocation($sender, $to, -25.9655, 32.5832, title: 'Smartek HQ');
// Contact card (phone is E.164; becomes the card's tap-to-chat number).
$mailer->whatsapp->sendContact($sender, $to, [
'name' => 'John Doe', 'phone' => '+258841112222', 'organization' => 'Acme Lda',
]);
// React to a message (outbound or inbound); '' removes the reaction.
$mailer->whatsapp->react($sender, $res['message_id'], '👍');Pass a third $options array to tune the client:
$mailer = new \SMailer\Client('CLT-XXXXXXXX', 'your-api-secret', [
'baseUrl' => 'https://api.mailer.smartek.co.mz', // override the API host
'maxRetries' => 2, // retry 429/5xx/network (default 2)
// Inject your own PSR-18 client / PSR-17 factories instead of auto-discovery:
// 'httpClient' => $psr18Client,
// 'requestFactory' => $psr17RequestFactory,
// 'streamFactory' => $psr17StreamFactory,
]);Retries use an exponential backoff and honour a Retry-After header on 429.
S-Mailer signs every webhook it sends with X-Mailer-Signature: hex(HMAC-SHA256(webhook_signing_key, raw_body)). Your webhook_signing_key comes from GET /api/v1/client ($mailer->balance->profile()). Verify over the raw request body, before decoding it.
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_MAILER_SIGNATURE'] ?? '';
$event = (new \SMailer\Webhook())->parse($raw, $sig, $signingKey); // throws on bad signature
if (($event['payment_required'] ?? false) === false) {
// handle inbound message / status update
}Two payload shapes are delivered, distinguished by their fields:
- Inbound:
{ id, from, to, channel, body, received_at, sender_id, client_id, payment_required } - Status:
{ message_id, recipient, status: { old, current }, error_code, timestamp }
Non-2xx responses raise a \SMailer\Exception\SMailerException subclass carrying the API error code (->errorCode) and HTTP status (->statusCode):
| Status | Exception (\SMailer\Exception\…) |
error |
|---|---|---|
| 401 | AuthenticationException |
AUTHENTICATION_FAILED |
| 403 | ForbiddenException |
FORBIDDEN |
| 400 | BadRequestException / ValidationException |
BAD_REQUEST / VALIDATION_ERROR |
| 402 | InsufficientTokensException |
INSUFFICIENT_TOKENS |
| 404 | NotFoundException |
NOT_FOUND |
| 429 | RateLimitException (->retryAfter) |
RATE_LIMIT_EXCEEDED |
| 5xx | ServerException |
INTERNAL_SERVER_ERROR |
Network/transport failures raise NetworkException.
use SMailer\Exception\InsufficientTokensException;
use SMailer\Exception\RateLimitException;
try {
$mailer->messages->send([/* ... */]);
} catch (InsufficientTokensException $e) {
// top up the account
} catch (RateLimitException $e) {
sleep($e->retryAfter ?? 1);
}MIT — see LICENSE.