nando/laravel

Official Laravel SDK for the Nando SMS public API — send messages and verify signed delivery webhooks.

Maintainers

Package info

github.com/gtech-ge/nando-php-sdk

Homepage

pkg:composer/nando/laravel

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-06-25 12:08 UTC

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.