threepp
C++20 기반 크로스플랫폼 3D 라이브러리, 파이썬 바인딩 및 three.js 수준 API 제공
하나의 REST 호출로 +255 번호에 SMS 전송, 멱등키·전달 보고 웹훅·테스트 모드 지원
Send SMS to +255 numbers from your own systems with one REST call: idempotency keys, delivery report webhooks and test mode.
개발 도구PHPGitHub @UNTU1995온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.