nando / laravel
Official Laravel SDK for the Nando SMS public API — send messages and verify signed delivery webhooks.
Requires
- php: ^8.1
- ext-sodium: *
- guzzlehttp/guzzle: ^7.5
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
This package is not auto-updated.
Last update: 2026-06-25 12:09:11 UTC
README
Official Laravel package for the Nando SMS public API. Send messages from your app and verify signed delivery webhooks — with a typed client, a fluent builder, and drop-in webhook middleware.
- ✅ Fluent SMS builder and a typed client
- ✅ First-class error handling (
NandoApiException) - ✅ Ed25519 webhook signature verification + middleware
- ✅ Config publishing and package auto-discovery
Requirements
- PHP 8.1+
- Laravel 10, 11, or 12
ext-sodium(bundled with PHP 7.2+)
Installation
composer require nando/laravel
The service provider and Nando facade are auto-discovered. Publish the config if you want to tweak it:
php artisan vendor:publish --tag=nando-config
Set your credentials in .env:
NANDO_API_KEY=sk_nando_live0123456789abcdef... NANDO_WEBHOOK_PUBLIC_KEY=base64-encoded-ed25519-public-key # optional NANDO_BASE_URL=https://nando.ge/api NANDO_TIMEOUT=15
Generate the API key and copy the webhook public key from the Nando platform under API.
Sending SMS
Fluent builder
use Nando\Laravel\Facades\Nando; Nando::sms() ->transactional() // ->otp() / ->regular() ->to('+995555123456') // or ->to(['+99555...', '+99557...']) ->body('Your verification code is 123456') ->send();
Pick the sender brand (defaults to your account's default brand when omitted) and schedule for later:
Nando::sms() ->brand(1) ->regular() ->to(['+995555111111', '+995555222222']) ->body('Big sale this weekend!') ->scheduledAt(now()->addHours(3)) ->send();
Data object or raw payload
sendSms() returns a Nando\Laravel\Data\Sms DTO and accepts either a SendSmsData DTO or a raw array:
use Nando\Laravel\Data\SendSmsData; use Nando\Laravel\Enums\SmsSubType; $sms = Nando::sendSms(new SendSmsData( body: 'Your verification code is 123456', recipients: ['+995555123456'], subType: SmsSubType::Transactional, )); $sms->id; // queued SMS id $sms->raw; // full server payload // ...or a plain array: Nando::sendSms([ 'sms_subtype' => 'transactional', 'sms_type' => 'one_time', 'body' => 'Your verification code is 123456', 'raw_phone_numbers' => ['+995555123456'], ]);
Account info
selfInfo() returns a typed Nando\Laravel\Data\AccountInfo:
$info = Nando::selfInfo(); $info->smsBalance; // int — remaining credits $info->brandNames; // list<BrandName> $info->defaultBrand(); // ?BrandName — used when a request omits brand_name_id $info->company; // array — company details Nando::test(); // bool — is the API key valid?
Architecture
The package is organised the way a Laravel developer would expect:
| Layer | Class | Responsibility |
|---|---|---|
| Contract | Contracts\NandoClientInterface |
Bind/type-hint this; swap or fake in tests |
| Client | NandoClient |
Composes repositories, exposes shortcuts |
| Transport | Http\Connector |
Auth, base URL, JSON, error handling |
| Repositories | Repositories\MessageRepository, AccountRepository |
Per-resource operations |
| DTOs | Data\SendSmsData, Sms, AccountInfo, BrandName |
Immutable, typed data |
| Webhooks | Webhook\SignatureVerifier, WebhookEvent |
Verification + parsed events |
use Nando\Laravel\Contracts\NandoClientInterface; public function __construct(private NandoClientInterface $nando) {} // Resource repositories are available directly: $this->nando->messages()->send($data); $this->nando->account()->info();
Error handling
Any non-2xx response throws a NandoApiException carrying the HTTP status and the parsed error envelope ({ "error": ... } or a { "errors": { field: message } } validation map):
use Nando\Laravel\Exceptions\NandoApiException; try { Nando::sms()->otp()->to('+995555123456')->body('123456')->send(); } catch (NandoApiException $e) { report($e); $e->status; // e.g. 400, 401, 403 $e->errors; // ['body' => 'is required'] or ['error' => '...'] $e->getMessage(); }
Webhooks
Nando POSTs a signed JSON event to your configured webhook URL whenever an API-originated SMS job changes status. Delivery is asynchronous: the send call returns immediately, and final statuses arrive later on your webhook.
Verify automatically with middleware
The package registers a nando.webhook middleware alias that rejects any request with a missing or invalid X-Nando-Signature (HTTP 403) before your controller runs:
use App\Http\Controllers\NandoWebhookController; Route::post('/webhooks/nando', NandoWebhookController::class) ->middleware('nando.webhook');
use Illuminate\Http\Request; use Nando\Laravel\Webhook\WebhookEvent; class NandoWebhookController { public function __invoke(Request $request) { $event = WebhookEvent::fromJson($request->getContent()); // Deduplicate — retries can deliver the same event more than once. if (Cache::add("nando:{$event->eventId}", true, now()->addDay())) { if ($event->hasFailures()) { // inspect $event->failedReceivers } // ...react to $event->currentStatus } return response()->noContent(); // 200 OK acknowledges delivery } }
Make sure no middleware mutates the raw body before verification. Signature checks run against the exact bytes Nando sent.
Verify manually
use Nando\Laravel\Webhook\SignatureVerifier; public function handle(Request $request, SignatureVerifier $verifier) { $ok = $verifier->verify( $request->getContent(), // raw body bytes $request->header('X-Nando-Signature'), // base64 signature ); abort_unless($ok, 403); }
Event payload
{
"event_id": "sms_job:123:processing:completed",
"event": "sms_job.status_changed",
"occurred_at": "2026-06-24T12:00:00Z",
"previous_status": "processing",
"current_status": "completed",
"sms_job": { "id": 123, "status": "completed", "brand_name": "Nando" },
"summary": { "total_receivers": 1, "delivered_count": 1, "failed_count": 0 },
"failed_receivers": []
}
Return 200 OK to acknowledge. Any other status, a timeout, or a network error triggers up to 5 retries with exponential backoff (1s, 2s, 4s, 8s, 16s).
Testing
composer install vendor/bin/phpunit
License
MIT. See LICENSE.