S-Mailer SDKs
Official SDKs for integrating with the S-Mailer API — send messages across SMS, WhatsApp, and email, list activity, check your balance, and verify webhooks.
Installation
composer require s-mailer/sdk
The SDK talks to any PSR-18 HTTP client. If you don't already have one installed:
composer require guzzlehttp/guzzle
Send a message
<?php
$mailer = new \SMailer\Client('CLT-XXXXXXXX', 'your-api-secret');
$result = $mailer->messages->send([
'sender' => 'sms-co1-xxxxxxxx',
'recipient' => '+258841234567',
'content' => 'Hello from S-Mailer!',
]);
echo $result['message_id'];
Send to many recipients with per-recipient template data:
$mailer->messages->send([
'sender' => 'sms-co1-xxxxxxxx',
'recipients' => [
'+258841234567',
['phone' => '+258842000001', 'data' => ['name' => 'Alice']],
],
'content_template' => 'Hi ${name}, your code is ${code}.',
]);
List, fetch, balance, channels
$sent = $mailer->messages->listOutbound(['status' => 'delivered', 'limit' => 50]);
$received = $mailer->messages->listInbound(['limit' => 50]);
$one = $mailer->messages->get($messageId); // outbound or inbound (see `direction`)
$balance = $mailer->balance->get(); // int — current token balance
$profile = $mailer->balance->profile(); // incl. webhook_signing_key
$channels = $mailer->channels->list(); // channels → providers → senders
Retrigger & delete
$mailer->messages->retrigger($inboundId); // charge + re-deliver a locked (unpaid) inbound
$mailer->messages->delete($messageId); // delete an outbound or paid inbound
WhatsApp advanced (media, buttons, location, contact, reactions)
WhatsApp senders only. Text is billed at the outbound cost; media, buttons, location, contact, and reactions at the advanced cost.
$sender = 'whatsapp-co1-xxxxxxxx';
$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'], '👍');
Verify webhooks
S-Mailer signs every webhook with
X-Mailer-Signature: hex(HMAC-SHA256(webhook_signing_key, raw_body)).
Your webhook_signing_key comes from $mailer->balance->profile().
Verify over the raw 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 the inbound message / status update
}
Errors
Non-2xx responses raise a \SMailer\Exception\SMailerException subclass
carrying ->errorCode and ->statusCode:
| Status | Exception | error |
|---|---|---|
| 401 | AuthenticationException | AUTHENTICATION_FAILED |
| 403 | ForbiddenException | FORBIDDEN |
| 400 | BadRequestException / ValidationException | BAD_REQUEST |
| 402 | InsufficientTokensException | INSUFFICIENT_TOKENS |
| 404 | NotFoundException | NOT_FOUND |
| 429 | RateLimitException (->retryAfter) | RATE_LIMIT_EXCEEDED |
Reference
Full method reference and options live in the repository README.
github.com/s-mailer/sdk-phpInstallation
npm install @s-mailer/sdk
Ships ESM + CommonJS with TypeScript types. Node 18+ (uses the global fetch).
Send a message
import { SMailer } from '@s-mailer/sdk';
const mailer = new SMailer({
clientId: 'CLT-XXXXXXXX',
clientSecret: 'your-api-secret',
});
const res = await mailer.messages.send({
sender: 'sms-co1-xxxxxxxx',
recipient: '+258841234567',
content: 'Hello from S-Mailer!',
});
console.log(res.message_id);
Send to many recipients with per-recipient template data:
await mailer.messages.send({
sender: 'sms-co1-xxxxxxxx',
recipients: [
'+258841234567',
{ phone: '+258842000001', data: { name: 'Alice' } },
],
contentTemplate: 'Hi ${name}, your code is ${code}.',
});
List, fetch, balance, channels
const sent = await mailer.messages.listOutbound({ status: 'delivered', limit: 50 });
const received = await mailer.messages.listInbound({ limit: 50 });
const one = await mailer.messages.get(messageId); // outbound or inbound (see `direction`)
const balance = await mailer.balance.get(); // number — current token balance
const profile = await mailer.balance.profile(); // incl. webhook_signing_key
const channels = await mailer.channels.list(); // channels → providers → senders
Retrigger & delete
await mailer.messages.retrigger(inboundId); // charge + re-deliver a locked (unpaid) inbound
await mailer.messages.delete(messageId); // delete an outbound or paid inbound
WhatsApp advanced (media, buttons, location, contact, reactions)
WhatsApp senders only. Text is billed at the outbound cost; media, buttons, location, contact, and reactions at the advanced cost.
const sender = 'whatsapp-co1-xxxxxxxx';
const to = '+258841234567';
await mailer.whatsapp.sendText(sender, to, 'Hello 👋');
// Host media anywhere publicly reachable, then pass the URL.
await mailer.whatsapp.sendMedia(sender, to,
{ kind: 'image', url: 'https://cdn.example.com/promo.jpg', filename: 'promo.jpg' },
'Our new catalogue');
const res = await mailer.whatsapp.sendButtons(sender, to, 'Confirm your appointment?', [
{ id: 'yes', title: 'Yes' },
{ id: 'no', title: 'No' },
], 'S-Mailer');
// Location pin.
await mailer.whatsapp.sendLocation(sender, to, -25.9655, 32.5832, 'Smartek HQ');
// Contact card (phone is E.164; becomes the card's tap-to-chat number).
await mailer.whatsapp.sendContact(sender, to,
{ name: 'John Doe', phone: '+258841112222', organization: 'Acme Lda' });
// React to a message (outbound or inbound); '' removes the reaction.
await mailer.whatsapp.react(sender, res.message_id, '👍');
Verify webhooks
S-Mailer signs every webhook with
X-Mailer-Signature: hex(HMAC-SHA256(webhook_signing_key, raw_body)).
Your webhook_signing_key comes from mailer.balance.profile().
Verify over the raw body, before parsing it.
import { webhooks } from '@s-mailer/sdk';
// Express — raw body required
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
const sig = req.header('X-Mailer-Signature') ?? '';
const event = webhooks.parse(raw, sig, signingKey); // throws on bad signature
if (event.type === 'inbound' && !event.payment_required) {
// handle the inbound message
} else if (event.type === 'status') {
// handle the delivery status update
}
res.sendStatus(200);
});
Errors
Non-2xx responses reject with a typed error extending SMailerError,
carrying errorCode and statusCode:
| Status | Error class | error |
|---|---|---|
| 401 | AuthenticationError | AUTHENTICATION_FAILED |
| 403 | ForbiddenError | FORBIDDEN |
| 400 | BadRequestError / ValidationError | BAD_REQUEST |
| 402 | InsufficientTokensError | INSUFFICIENT_TOKENS |
| 404 | NotFoundError | NOT_FOUND |
| 429 | RateLimitError (retryAfter) | RATE_LIMIT_EXCEEDED |
Reference
Full method reference and options live in the repository README.
github.com/s-mailer/sdk-node