Looking to hire Laravel developers? Try LaraJobs

laravel-taqnyat maintained by sevaske

Description
Laravel integration for the Taqnyat SMS API (taqnyat.sa): facade, notification channel, templates from config, test fake and artisan commands.
Author
sevaske
Last update
2026/11/05 16:33 (dev-main)
License
Downloads
1

Comments
comments powered by Disqus

Taqnyat SMS for Laravel

CI Latest Version PHP Version License

Laravel integration for the Taqnyat SMS API, built on top of the framework-agnostic sevaske/taqnyat-api client.

  • Taqnyat facade with a default sender and templates from config
  • Notification channel: send notifications as SMS
  • Taqnyat::fake() with assertions for your tests
  • Events, artisan commands (taqnyat:send, taqnyat:balance, taqnyat:senders, taqnyat:status)
  • Laravel 10 – 13, PHP 8.1 – 8.5

This is an unofficial, community-maintained package and is not affiliated with Taqnyat.

Installation

composer require sevaske/laravel-taqnyat

The service provider and the Taqnyat facade are registered automatically (package discovery).

Add your credentials to .env:

TAQNYAT_TOKEN=your-bearer-token
TAQNYAT_SENDER=YourSenderName

To get a bearer token, log in to your Taqnyat account, open Developer → Application, add an application with the SMS service enabled and copy its bearer token (see Setup Bearer token). The sender must be an active sender name in your account. List them with php artisan taqnyat:senders.

Optionally publish the config file:

php artisan vendor:publish --tag=taqnyat-config
Env variable Default Description
TAQNYAT_TOKEN none Bearer token (required)
TAQNYAT_SENDER none Default sender name
TAQNYAT_TIMEZONE Asia/Riyadh Timezone scheduled dates are converted to
TAQNYAT_BASE_URI https://api.taqnyat.sa API base URI
TAQNYAT_TIMEOUT 10 Request timeout, seconds
TAQNYAT_CONNECT_TIMEOUT 5 Connection timeout, seconds

Sending SMS

use Sevaske\LaravelTaqnyat\Facades\Taqnyat;

// Plain text, default sender from config
Taqnyat::sendText('966500000000', 'Your order has shipped');

// Several recipients, explicit sender
Taqnyat::sendText(['+966 50 000 0000', '00966511111111'], 'Hello!', 'OtherSender');

// Scheduled message: build it with the default sender, then send
Taqnyat::send(
    Taqnyat::message('966500000000', 'Reminder: your appointment is tomorrow')
        ->scheduleAt(now()->addDay())
        ->withDeleteId(1001) // optional: lets you cancel it with Taqnyat::delete(1001)
);

Every send method returns Sevaske\Taqnyat\Response\SendSmsResponse:

$response = Taqnyat::sendText($phone, 'Hello');

$response->getMessageId(); // "5829452722"
$response->getCost();      // "0.1500"
$response->hasRejected();  // true if some recipients were rejected

Phone numbers are normalized automatically (+966…, 00966…, spaces and dashes are fine); invalid numbers throw Sevaske\Taqnyat\Exception\InvalidArgumentException before any request is made.

Templates

Define message bodies with {placeholders} in config/taqnyat.php:

'templates' => [
    'otp'      => 'Your verification code is {code}. It expires in {minutes} minutes.',
    'shipping' => 'Hi {name}, your order #{order} has shipped.',
],
Taqnyat::sendTemplate($user->phone, 'otp', ['code' => $code, 'minutes' => 5]);

Rendering is strict: a missing placeholder value throws an exception, so a customer never receives Your verification code is {code}. You can also pass a Sevaske\Taqnyat\Template\Template instance instead of a name.

Notifications

Return 'taqnyat' (or TaqnyatChannel::class) from via() and implement toTaqnyat():

use Illuminate\Notifications\Notification;
use Sevaske\LaravelTaqnyat\Notifications\TaqnyatMessage;

class OrderShipped extends Notification
{
    public function __construct(private Order $order) {}

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

    public function toTaqnyat($notifiable): TaqnyatMessage
    {
        return TaqnyatMessage::create("Your order #{$this->order->number} has shipped.");
    }
}

toTaqnyat() may return:

  • a string: the message text;
  • a TaqnyatMessage: text or template plus options (below);
  • an Sevaske\Taqnyat\Message\SmsMessage: sent as is, with its own sender and recipients.
TaqnyatMessage::create('Plain text')            // or ->content('...')
    ->template('shipping', ['name' => 'Sara', 'order' => 1042]) // instead of text
    ->sender('OtherSender')                     // instead of TAQNYAT_SENDER
    ->to('966511111111')                        // instead of the notifiable's phone
    ->scheduleAt(now()->addHour())
    ->deleteId(1001);

Tell Laravel where to send SMS for your notifiable:

class User extends Authenticatable
{
    use Notifiable;

    public function routeNotificationForTaqnyat($notification): ?string
    {
        return $this->phone; // a string, or an array of numbers
    }
}

If the route is empty, the notification is skipped for that notifiable without an error.

On-demand notifications:

use Illuminate\Support\Facades\Notification;

Notification::route('taqnyat', '966500000000')->notify(new OrderShipped($order));

Queued notifications (ShouldQueue) work as usual.

Other endpoints

Taqnyat::balance();   // BalanceResponse: getBalance(), getCurrency(), getPoints(), getExpiryDate()...
Taqnyat::senders();   // SendersResponse: iterable list of Sender, getNames(), getActive(), find()...
Taqnyat::status();    // SystemStatusResponse: isOperational(), getDescription()
Taqnyat::delete(1001); // deletes a scheduled message by its delete ID

See the sevaske/taqnyat-api documentation for all response methods.

Dependency injection

Instead of the facade you can inject:

Class What it is
Sevaske\LaravelTaqnyat\Taqnyat The service behind the facade (default sender, templates, events)
Sevaske\Taqnyat\Contracts\SmsSender Minimal send(SmsMessage) contract, faked by Taqnyat::fake()
Sevaske\Taqnyat\Client The underlying framework-agnostic client
Sevaske\Taqnyat\Template\TemplateRegistry Templates from config
use Sevaske\LaravelTaqnyat\Taqnyat;

class SendOtp
{
    public function __construct(private Taqnyat $taqnyat) {}

    public function __invoke(User $user, string $code): void
    {
        $this->taqnyat->sendTemplate($user->phone, 'otp', ['code' => $code]);
    }
}

Events

Event When Properties
Sevaske\LaravelTaqnyat\Events\MessageSending Before a message is sent $message
Sevaske\LaravelTaqnyat\Events\MessageSent After Taqnyat accepted it $message, $response

Events are dispatched for messages sent through the facade, the Taqnyat service and the notification channel.

Artisan commands

php artisan taqnyat:status     # service status (exit code 1 during maintenance)
php artisan taqnyat:balance    # account balance, points, expiry date
php artisan taqnyat:senders    # your sender names and their status
php artisan taqnyat:send 966500000000 --message="Hello"
php artisan taqnyat:send 966500000000 966511111111 --template=otp --param=code=1234 --sender=MyBrand

Testing

Taqnyat::fake() replaces real sending with a fake that records messages. No HTTP requests are made and no token is needed:

use Sevaske\LaravelTaqnyat\Facades\Taqnyat;
use Sevaske\Taqnyat\Message\SmsMessage;

public function test_otp_is_sent(): void
{
    $sms = Taqnyat::fake();

    $this->post('/login/otp', ['phone' => '+966500000000']);

    $sms->assertSentTo('966500000000', fn (SmsMessage $message) => str_contains($message->getBody(), 'code'));
    $sms->assertSentCount(1);
}

Available assertions: assertSent(?callable), assertSentTo($phone, ?callable), assertNotSentTo($phone), assertNotSent(callable), assertNothingSent(), assertSentCount(int). Use sent(?callable) and sentTo($phone) to inspect the messages yourself.

Simulate failures:

use Sevaske\Taqnyat\Exception\AuthenticationException;

Taqnyat::fake()->throwOnSend(new AuthenticationException('invalid credentials information.', 401));

The fake covers the facade, the Taqnyat service, the notification channel and the SmsSender contract. Other endpoints (balance(), senders()...) and a directly injected Sevaske\Taqnyat\Client are not faked; mock the facade for them instead:

use Sevaske\Taqnyat\Response\BalanceResponse;

Taqnyat::shouldReceive('balance')->once()->andReturn(BalanceResponse::fromArray(['balance' => '100.00']));

Customizing the HTTP client

By default a Guzzle client with the configured timeouts is used. Rebind it to add a proxy, retries or logging:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Sevaske\LaravelTaqnyat\TaqnyatServiceProvider;

// In AppServiceProvider::register()
$this->app->singleton(TaqnyatServiceProvider::HTTP_CLIENT, function () {
    $stack = HandlerStack::create();
    $stack->push(Middleware::retry(fn ($retries, $request, $response, $error) => $retries < 2 && $error !== null));

    return new Client(['handler' => $stack, 'timeout' => 10]);
});

Any PSR-18 client works.

Error handling

All exceptions implement Sevaske\Taqnyat\Exception\TaqnyatException. API errors are thrown as subclasses of ApiException (AuthenticationException, BadRequestException...); see the core library docs. Missing configuration (token or sender) throws Sevaske\LaravelTaqnyat\Exceptions\ConfigurationException, but only when the value is actually needed, so your application boots without a token.

Development

composer install
composer test           # PHPUnit (Orchestra Testbench)
composer analyse        # PHPStan (level max)
composer pint:install   # once, requires PHP 8.3+
composer format         # Laravel Pint

License

The MIT License (MIT). See LICENSE.