DocumentationAPI v1 · 1.3.0

Packages

Laravel package

Install labyrinthe/connect-laravel: facade, notification channel, validation rules, webhooks and Labyrinthe::fake().

Laravel 10 to 13 integration of the PHP SDK: .env configuration, Labyrinthe facade, labyrinthe notification channel, PhoneNumber and ValidOtp rules, webhook route with events, and a test fake.

Version 1.2.0. Source code, examples and changelog: laravel folder on GitHub.

Installation#

Shell
composer require labyrinthe/connect-laravel

# Optional: publish the config file and the translations
php artisan vendor:publish --tag=labyrinthe-config
php artisan vendor:publish --tag=labyrinthe-lang

The service provider and facade are auto-discovered.

Configuration#

.env
LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
LABYRINTHE_SENDER=MABOUTIQUE
LABYRINTHE_WEBHOOK_SECRET=whsec_from_the_client_area
# LABYRINTHE_TIMEOUT=30
# LABYRINTHE_MAX_RETRIES=2
# LABYRINTHE_OTP_LOCALE=fr
config/labyrinthe.php keyVariableDefault
api_keyLABYRINTHE_API_KEY—
base_urlLABYRINTHE_BASE_URLProduction
timeout, max_retriesLABYRINTHE_TIMEOUT, LABYRINTHE_MAX_RETRIES30, 2
senderLABYRINTHE_SENDER—
otp.template, otp.locale, otp.web_otp_domain, otp.strip_accentsLABYRINTHE_OTP_*—
webhook.secret, webhook.pathLABYRINTHE_WEBHOOK_SECRET, LABYRINTHE_WEBHOOK_PATHwebhooks/labyrinthe
webhook.deduplicate, webhook.toleranceLABYRINTHE_WEBHOOK_DEDUPLICATE, LABYRINTHE_WEBHOOK_TOLERANCEtrue, 300

Notification channel#

Laravel
<?php
use Illuminate\Notifications\Notification;
use Labyrinthe\Connect\Laravel\Notifications\LabyrintheMessage;

class OrderShipped extends Notification
{
    public function __construct(private string $orderNumber) {}

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

    public function toLabyrinthe(mixed $notifiable): LabyrintheMessage|string
    {
        return LabyrintheMessage::create("Votre commande {$this->orderNumber} est expédiée.")
            ->sender('MABOUTIQUE')
            ->reference('order-'.$this->orderNumber);
    }
}

// Recipient: routeNotificationForLabyrinthe(), then the "phone" attribute
$user->notify(new OrderShipped('1042'));
Notification::route('labyrinthe', '0812345678')->notify(new OrderShipped('1042'));

The default Idempotency-Key is laravel-notification-<notification id>: a notification retried by the queue does not send two SMS.

Send an SMS#

Laravel
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$message = Labyrinthe::sms()->send(
    to: '243812345678',
    body: 'Votre commande 1042 est prête.',
    reference: 'CMD-1042',
    idempotencyKey: 'order-1042-ready',
);

// Shortcut: Labyrinthe::send('243812345678', 'Votre commande 1042 est prête.');

Several recipients (up to 1000, all or nothing):

Laravel
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$batch = Labyrinthe::sms()->sendMany(
    recipients: ['243812345678', '0899999999'],
    body: 'Rappel : reunion demain 9h.',
    sender: 'MABOUTIQUE',
    reference: 'RAPPEL-2026-11-06',
    idempotencyKey: 'reminder-2026-11-06',
);

count($batch); // 2 messages, one per recipient

Estimate the cost, read the balance, list messages:

Laravel
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$estimate = Labyrinthe::sms()->estimate(
    body: 'Votre réservation est confirmée.',
    recipients: ['243812345678', '0899999999'],
    stripAccents: true, // GSM-7: often half the price
);

$estimate->totalCredits;
Laravel
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$balance = Labyrinthe::balance()->get();

if ($balance->balance < 100) {
    // alert the team
}
Laravel
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$page = Labyrinthe::sms()->list(reference: 'CMD-1042');

foreach ($page->autoPaging() as $message) {
    logger()->info($message->id.' '.$message->status->value);
}

OTP verification#

1. Send the code
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$otp = Labyrinthe::otp()->send(
    $request->input('phone'),
    idempotencyKey: 'login-'.$request->session()->getId(),
);

session(['otp_id' => $otp->otpId]);
2. Verify the code
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;
use Labyrinthe\Connect\Laravel\Rules\ValidOtp;

// As a validation rule (each wrong code uses an attempt)
$request->validate([
    'code' => ['required', 'string', new ValidOtp(session('otp_id'))],
]);

// Or by hand
if (Labyrinthe::otp()->check(session('otp_id'), $request->input('code'))) {
    session()->forget('otp_id');
}

Receive webhooks#

Laravel
<?php
// .env: LABYRINTHE_WEBHOOK_SECRET=…
// routes/web.php: signature, timestamp, deduplication and CSRF exemption are handled
Route::labyrintheWebhooks(); // POST /webhooks/labyrinthe

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Event;
use Labyrinthe\Connect\Laravel\Events\MessageStatusChanged;

Event::listen(function (MessageStatusChanged $e) {
    if ($e->event->status->isFinal()) {
        Order::where('reference', $e->event->reference)->update(['sms_status' => $e->event->status->value]);
    }
});

Errors and maintenance#

Maintenance (503 FEATURE_DISABLED and OTP_SERVICE_DISABLED) has its own exception: nothing was sent or debited. The package retries on its own if Retry-After is short (10 s by default); otherwise, queue the send. See Errors.

Laravel
<?php
// .env: LABYRINTHE_API_KEY=lsk_test_xxxxxxxxxxxxxxxx
use Labyrinthe\Connect\Exception\ApiException;
use Labyrinthe\Connect\Exception\FeatureDisabledException;
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

try {
    Labyrinthe::send('243812345678', 'Bonjour');
} catch (FeatureDisabledException $e) {
    // Maintenance: release the job and try again later instead of failing it
    $this->release($e->getRetryAfter() ?? 300);
} catch (ApiException $e) {
    report($e); // $e->getErrorCode(), $e->getRequestId()
}

// In tests:
// Labyrinthe::fake()->failNextRequestWithFeatureDisabled('api.sms_send', 'Maintenance.', now()->addHour());

Testing: Labyrinthe::fake()#

Laravel
<?php
use Labyrinthe\Connect\Laravel\Facades\Labyrinthe;

$fake = Labyrinthe::fake()->withBalance(500);

$this->post('/orders/1042/ship');

Labyrinthe::assertSmsSentTo('0812345678', fn ($sms) => str_contains($sms->body, '1042'));
Labyrinthe::assertSmsSentCount(1);

// OTP: the fake knows the code
$otp = Labyrinthe::otp()->send('0812345678');
$code = $fake->otpCode('+243812345678');

// Simulated maintenance
Labyrinthe::fake()->failNextRequestWithFeatureDisabled('api.sms_send', 'Maintenance.', now()->addHour());