farsi / laravel-smsir
A modern Laravel package for the sms.ir v1 API — sending, reports, inbox, and a notification channel.
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.2
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- spatie/laravel-package-tools: ^1.93
Requires (Dev)
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^7.9|^8.0
- orchestra/testbench: ^8.0|^9.2|^10.0|^11.0
- pestphp/pest: ^2.34|^3.0|^4.0
- pestphp/pest-plugin-laravel: ^2.3|^3.0|^4.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1|^2.0
- phpstan/phpstan-phpunit: ^1.3|^2.0
README
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.