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#
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-langThe service provider and facade are auto-discovered.
Configuration#
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=frconfig/labyrinthe.php key | Variable | Default |
|---|---|---|
api_key | LABYRINTHE_API_KEY | — |
base_url | LABYRINTHE_BASE_URL | Production |
timeout, max_retries | LABYRINTHE_TIMEOUT, LABYRINTHE_MAX_RETRIES | 30, 2 |
sender | LABYRINTHE_SENDER | — |
otp.template, otp.locale, otp.web_otp_domain, otp.strip_accents | LABYRINTHE_OTP_* | — |
webhook.secret, webhook.path | LABYRINTHE_WEBHOOK_SECRET, LABYRINTHE_WEBHOOK_PATH | webhooks/labyrinthe |
webhook.deduplicate, webhook.tolerance | LABYRINTHE_WEBHOOK_DEDUPLICATE, LABYRINTHE_WEBHOOK_TOLERANCE | true, 300 |
Notification channel#
<?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#
<?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):
<?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 recipientEstimate the cost, read the balance, list messages:
<?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;<?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
}<?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#
<?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]);<?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#
<?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.
<?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()#
<?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());