farsi/laravel-smsir

A modern Laravel package for the sms.ir v1 API — sending, reports, inbox, and a notification channel.

Maintainers

Package info

github.com/farsidev/laravel-smsir

pkg:composer/farsi/laravel-smsir

Transparency log

Statistics

Installs: 69

Dependents: 0

Suggesters: 0

Stars: 0

v1.1.1 2026-08-02 20:10 UTC

This package is auto-updated.

Last update: 2026-08-03 21:19:15 UTC


README

tests phpstan packagist license

A modern Laravel client for the sms.ir v1 API — sending, delivery reports, the inbox, and a notification channel.

Supports Laravel 10, 11, 12 and 13 on PHP 8.2+.

Installation

composer require farsi/laravel-smsir

Publish the config:

php artisan vendor:publish --tag=smsir-config

Then set your credentials:

SMSIR_API_KEY=your-api-key
SMSIR_LINE_NUMBER=30007

Usage

use Farsi\Smsir\Facades\Smsir;

// One message to one recipient
Smsir::send('09120000000', 'سلام!');

// The same message to many
Smsir::bulk('تخفیف ویژه امروز', ['09120000000', '09120000001']);

// A different message to each recipient, in one request
Smsir::likeTolike(['سلام علی', 'سلام مریم'], ['09120000000', '09120000001']);

// A template (verification) message — the OTP shape
Smsir::verify('09120000000', templateId: 100, ['CODE' => '1234']);

// Scheduled
Smsir::bulk('یادآوری', $mobiles, sendAt: now()->addHour());
Smsir::cancelScheduled($packId);

Recipients are accepted in any common Iranian format and normalised for you — 09120000000, 9120000000, +989120000000 and 00989120000000 are equivalent.

Named templates

Rather than scattering template ids through your code, name them in config:

// config/smsir.php
'templates' => [
    'otp' => env('SMSIR_TEMPLATE_OTP'),
],
Smsir::verify('09120000000', 'otp', ['CODE' => $code]);

Reports and inbox

Smsir::reports()->byId($messageId);
Smsir::reports()->byPack($packId);
Smsir::reports()->packs(page: 1, perPage: 50);
Smsir::reports()->live(mobile: '09120000000');
Smsir::reports()->archive(from: now()->subWeek(), to: now());

Smsir::received()->latest(50);
Smsir::received()->live();
Smsir::received()->archive(from: now()->subMonth(), to: now());

Smsir::credit();   // float
Smsir::lines();    // ['30007', ...]

Dates come back as CarbonImmutable, and mobile numbers come back correctly — see Two upstream quirks.

Notification channel

use Farsi\Smsir\Notifications\SmsirMessage;
use Illuminate\Notifications\Notification;

class SendOtp extends Notification
{
    public function __construct(protected string $code) {}

    public function via($notifiable): array
    {
        return ['smsir'];
    }

    public function toSmsir($notifiable): SmsirMessage
    {
        return SmsirMessage::verify('otp')->with(['CODE' => $this->code]);
    }
}
$user->notify(new SendOtp($code));

Plain text works too:

return SmsirMessage::text('سفارش شما ارسال شد.');

The recipient is taken from routeNotificationFor('smsir') when you define it, and otherwise from a mobile, phone or phone_number attribute. A notifiable with no number is skipped rather than treated as an error.

Events

use Farsi\Smsir\Events\{SmsSending, SmsSent, SmsFailed};

Event::listen(SmsSent::class, function (SmsSent $event) {
    logger()->info("Sent {$event->packId} for {$event->cost} credits");
});

SmsSending carries a mutable payload, so you can redirect every message to a test number outside production:

Event::listen(SmsSending::class, function (SmsSending $event) {
    if (! app()->isProduction()) {
        $event->payload['mobiles'] = ['09120000000'];
    }
});

Database logging (optional)

Off by default. To switch it on:

SMSIR_LOG=true
php artisan vendor:publish --tag=smsir-migrations
php artisan migrate

One row per recipient per attempt, successes and failures alike. The model is swappable via smsir.logging.model. Logging can never break a send — if the write fails, the failure is swallowed, because losing a log row is strictly better than turning a delivered message into an application error.

Error handling

use Farsi\Smsir\Exceptions\{SmsirException, InvalidApiKeyException, ConnectionFailedException};

try {
    Smsir::send('09120000000', 'سلام');
} catch (InvalidApiKeyException $e) {
    // envelope status 10
} catch (ConnectionFailedException $e) {
    // no answer at all — the message may still have been sent
} catch (SmsirException $e) {
    $e->statusCode;   // upstream status
    $e->getMessage(); // upstream Persian message, verbatim
}

Prefer failing soft? Set SMSIR_THROW=false and calls return null (or a zero credit) instead of throwing. Events still fire either way, so logging and alerting behave identically under both settings.

ConnectionFailedException is deliberately distinct from a rejection: a timeout means the message may well have been sent, so retrying it risks a duplicate.

Two upstream quirks, handled for you

Mobile numbers lose their leading zero. sms.ir types them as int64, so 09120000000 comes back as 9120000000. A naive client silently corrupts every number it reads back. This package restores them.

Dates are Unix int32, not strings. You pass Carbon, you get CarbonImmutable — the integer conversion happens at the boundary.

What this package deliberately does not do

The legacy URL-send endpoint (GET|POST /v1/send) is not implemented. It authenticates with your account username and password as query parameters, which puts credentials into access logs, proxies and referrer headers. bulk does the same job under API-key auth, so Smsir::send() uses that instead.

The package also ships no routes, no views, and no assets.

Testing

Everything goes through Laravel's HTTP client, so Http::fake() works out of the box:

Http::fake(['api.sms.ir/*' => Http::response([
    'status' => 1, 'message' => 'موفق',
    'data' => ['messageId' => 1, 'cost' => 1],
])]);

Smsir::verify('09120000000', 'otp', ['CODE' => '1234']);

Http::assertSent(fn ($request) => $request['mobile'] === '09120000000');

The package's own suite never touches the network.

composer test
composer analyse
composer format

Upstream drift

resources/openapi/smsir-v1.json is a vendored copy of the published OpenAPI document, and a test asserts the package's endpoint map still matches it. If sms.ir removes an endpoint or adds one, CI fails rather than production. Refresh it with:

curl -sk https://api.sms.ir/swagger/v1/swagger.json -o resources/openapi/smsir-v1.json

Credits

Forked from IPeCompany/SmsirLaravel (© 2017 PHPlus, authored by moein.alizadeh). That package targeted the retired ws.sms.ir token API and had not been released since 2018; this is a rewrite against the current v1 API, retaining its MIT licence and original copyright.

License

MIT. See LICENSE.md.