尚未偵測到更新。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.