尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证PHP SDK for Sendd: send SMS to Tanzanian mobile numbers, with safe retries, previews and webhook verification (MIT).
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Send SMS to Tanzanian mobile numbers with Sendd from PHP 8.1 or later.
composer config repositories.sendd vcs https://github.com/UNTU1995/sendd-php
composer require sendd/sendd
$sendd = new Sendd\Client(); // the key from SENDD_API_KEY
$result = $sendd->messages->send(
to: '0754 123 456',
from: 'MYSHOP',
body: 'Habari! Oda yako #1042 iko tayari.',
clientRef: 'order-1042',
);
echo $result->message['id'], ' ', $result->message['status']; // "accepted": recorded and paid for, not yet delivered
Start with a test key (sndd_test_…, from the dashboard's API keys page). Test messages never reach a phone and are never charged; they finish at once with an outcome set by the number's last digit: 1 or 2 failed, 8 undelivered, anything else delivered.
Needs ext-curl, ext-json and ext-mbstring, and has no other dependencies. ext-intl is suggested: with it, text is normalized to Unicode NFC before counting, exactly as Sendd counts. Without it, text is counted as given, which differs from Sendd's count only for text in decomposed form (a letter followed by a separate combining accent).
429 or a 5xx (not 501), the SDK sends again with the same key and the same message, so Sendd answers with the first result and never sends or charges twice. It waits out Retry-After up to 30 seconds (maxRetryWait); a longer wait comes back as the exception's retryAfter. A refusal (any other 4xx) is never retried. Redirects are never followed.accepted and submitted are not delivered. unknown means the outcome is unclear and Sendd is checking it: never send that message again yourself.messages->preview() counts segments exactly as Sendd will, checks the characters the SMS route takes, prices the message from your rates and checks the sender ID and your balance, as far as the key may read them.$preview = $sendd->messages->preview(body: $text, to: '0754123456', from: 'MYSHOP');
if (!$preview->ok) {
// Each problem has the code Sendd would refuse with: encoding_not_supported,
// too_many_segments, sender_not_authorized, sender_not_ready, insufficient_funds, …
foreach ($preview->problems as $problem) {
echo $problem->code, ': ', $problem->message, "\n";
}
} else {
echo "{$preview->segments} segment(s), {$preview->total} cents\n";
}
$result = $sendd->messages->send(
to: '0754123456',
from: 'MYSHOP',
body: $text,
clientRef: 'order-1042',
ttl: 300,
idempotencyKey: 'order-1042-ready', // optional: makes the send idempotent across restarts too
);
$result->message; // the message, as the API returns it (snake_case fields)
$result->idempotencyKey; // keep it to retry this exact send
$result->replayed; // true: Sendd answered with an earlier identical send's result
$final = $sendd->messages->waitForFinal($result->message['id'], timeout: 60);
The sender ID must be spelled exactly as listed by $sendd->senderIds->list(), case included.
Some routes take plain ASCII text only ($sendd->serviceStatus()['sms_route'] === 'ascii'): accented letters, curly quotes, long dashes and emoji are refused with encoding_not_supported and the exact characters and positions, and Sendd never removes or replaces them for you. Sendd\Text::countOn($body, $route) counts a body as Sendd will; Sendd\Phone::normalize($number) gives a number in E.164, or null when Sendd would refuse it.
Every failed call throws Sendd\SenddException:
use Sendd\SenddException;
try {
$sendd->messages->send(to: $to, from: 'MYSHOP', body: $text, clientRef: $ref);
} catch (SenddException $e) {
$e->errorCode; // "insufficient_funds", "rate_limited", "network_error", "timeout", "invalid_response", …
$e->status; // HTTP status, or null when no answer arrived
$e->field; // "to", "from", "body", … when the error is about one field
$e->details; // for encoding_not_supported: the characters refused
$e->retryAfter; // seconds Sendd asked to wait (429, 503)
$e->requestId; // quote it to Sendd support
$e->isRetryable(); // true: the same request may be sent again unchanged
$e->isOutcomeUnknown(); // true: Sendd may have accepted it; retry with $e->idempotencyKey
}
getCode() is always 0 (PHP's exception code is an integer); the string code is $e->errorCode (or getErrorCode()). A client set up wrongly (no key, plain http to another machine) throws invalid_configuration from the constructor.
foreach ($sendd->messages->all(['status' => 'failed']) as $m) { // every page, as a generator
echo $m['id'], ' ', $m['failure_reason'], "\n";
}
$page = $sendd->messages->list(['limit' => 50]); // $page->items, $page->nextBefore
$message = $sendd->messages->get($id);
$wallet = $sendd->wallet->get(); // cents (1/100 TZS)
$statement = $sendd->wallet->statement(['limit' => 20]);
$rates = $sendd->pricing(); // cents per segment
$senders = $sendd->senderIds->list();
$key = $sendd->key(); // mode and scopes
$campaigns = $sendd->campaigns->list();
$deliveries = $sendd->webhookDeliveries->list(['state' => 'failed']);
Each call needs the key to hold its scope: messages:send, messages:read, sender_ids:read, wallet:read, campaigns:read, webhooks:read. A missing scope is insufficient_scope (403). An unknown filter name throws InvalidArgumentException rather than being ignored.
Data shapes. What the API returns comes back as plain arrays with the API's own snake_case field names (client_ref, failure_reason, next_before), so the API reference describes them exactly. What the SDK itself works out comes back as readonly value objects: SendResult, Page, Preview, Problem and TextCount.
Verify the raw body (before any JSON parsing) against the Sendd-Signature header:
use Sendd\Webhook;
use Sendd\WebhookVerificationException;
$raw = file_get_contents('php://input');
try {
$event = Webhook::verify(
$raw,
$_SERVER['HTTP_SENDD_SIGNATURE'] ?? null,
array_filter([getenv('SENDD_WEBHOOK_SECRET'), getenv('SENDD_WEBHOOK_SECRET_PREVIOUS')]),
);
// Events can arrive more than once and out of order: de-duplicate on $event['id'].
http_response_code(204);
} catch (WebhookVerificationException $e) {
http_response_code(400); // $e->reason says why
}
Webhook::signForTest($body, $secret) makes a valid header, to test a handler without Sendd. The sendd CLI can also post a signed test event to a local handler: sendd webhooks trigger --url http://localhost:8000/webhooks/sendd.
Keep the key and the webhook secret in .env, and read them through config/services.php (never env() outside config files: it returns null once the config is cached):
// config/services.php
'sendd' => [
'key' => env('SENDD_API_KEY'),
'webhook_secret' => env('SENDD_WEBHOOK_SECRET'),
],
// app/Providers/AppServiceProvider.php, in register()
$this->app->singleton(\Sendd\Client::class, fn () => new \Sendd\Client(apiKey: config('services.sendd.key')));
Then type-hint Sendd\Client in a controller or job. Send from a queued job, so a slow network never holds up a web request, and pass a stable idempotencyKey (for example "order-{$order->id}-ready") so a retried job never sends twice.
A webhook route must read the raw body, and is a POST from Sendd with no CSRF token. Put it in routes/api.php (no CSRF there), or exclude it from CSRF in bootstrap/app.php ($middleware->validateCsrfTokens(except: ['webhooks/sendd'])):
// routes/api.php
use Illuminate\Http\Request;
use Sendd\Webhook;
use Sendd\WebhookVerificationException;
Route::post('/webhooks/sendd', function (Request $request) {
try {
$event = Webhook::verify($request->getContent(), $request->header('Sendd-Signature'), config('services.sendd.webhook_secret'));
} catch (WebhookVerificationException) {
return response()->noContent(400);
}
// Hand $event to a queued job; de-duplicate on $event['id'].
return response()->noContent();
});
$request->getContent() is the exact body received; $request->all() or $request->json() is parsed and will not verify.
| Argument | Default |
|---|---|
apiKey | SENDD_API_KEY |
baseUrl | SENDD_API_URL, else https://app.sendd.africa (plain http only to localhost) |
timeout | 30 seconds per attempt |
maxRetries | 2 |
maxRetryWait | 30 seconds |
transport | Sendd\Http\CurlTransport (implement Sendd\Http\HttpTransport to replace it, for example in tests) |
userAgent | appended to sendd-php/<version> php/<version> |
composer install
vendor/bin/phpunit --testsuite unit
The unit tests check this SDK against the same fixtures every Sendd client is held to (tests/fixtures/: segment counting, phone numbers, webhook signatures and answers recorded from the real API). The conformance suite runs the shared cases against a live Sendd test environment and is skipped unless SENDD_CONFORMANCE_FIXTURE names one; Sendd runs it on every release.
Semantic versioning; see CHANGELOG.md. Breaking changes come only in a new major version, after a deprecation period.