ovh/php-ovh-sms
A lightweight PHP SDK for the OVH SMS API. Send transactional and marketing SMS, manage sender IDs, check delivery reports, and integrate OVH messaging into Laravel or any PHP project via Composer. Simple, fluent setup with your OVH credentials—ideal for alerts, notifications, and 2FA codes.
1. Install and pin. The package is Composer-only and hasn't seen a release since 2016, so add it explicitly and commit the lock file:
composer require ovh/php-ovh-sms
Check composer show ovh/php-ovh-sms afterwards — you're looking at the composer.json require block (PHP version, Guzzle version) to confirm it resolves cleanly on your Laravel version. If you're on PHP 8.x, run a smoke test in php artisan tinker before wiring it into anything real.
2. Get credentials before writing code. Log into the OVH API console for your endpoint (https://eu.api.ovh.com/createApp/, https://ca.api.ovh.com/createApp/, etc.) and create an application. You get an application key and application secret. Then create a consumer key scoped to the SMS endpoints you actually need (GET /sms/*, POST /sms/* for the account, or narrower like POST /sms/{serviceName}/jobs). Write down all four values: endpoint, app key, app secret, consumer key.
3. Store them in config/services.php. The SDK has no Laravel config of its own, so this is your job:
'ovh_sms' => [
'endpoint' => env('OVH_SMS_ENDPOINT', 'ovh-eu'),
'app_key' => env('OVH_SMS_APP_KEY'),
'app_secret' => env('OVH_SMS_APP_SECRET'),
'consumer_key'=> env('OVH_SMS_CONSUMER_KEY'),
'sender' => env('OVH_SMS_SENDER'),
'service' => env('OVH_SMS_SERVICE'), // your sms-xx-1 account name
],
4. Bind it in a service provider. There is no OvhSmsServiceProvider shipped, so register a singleton:
$this->app->singleton(SmsApi::class, function ($app) {
$c = $app['config']['services.ovh_sms'];
return new SmsApi($c['endpoint'], $c['app_key'], $c['app_secret'], $c['consumer_key']);
});
Confirm the constructor signature against the README of the exact tag you installed — it has changed across forks, and getting the argument order wrong produces confusing "invalid signature" errors rather than a type error.
First use case: replace a mail()-based "welcome" SMS or an order-confirmation text. Send one message from a queued job, log the returned job ID, and verify it in the OVH manager before building anything else on top. Sending one real SMS end-to-end is the fastest way to catch a wrong endpoint or an unscoped consumer key.
Wrap the SDK behind your own interface. Because the library is old and unlikely to be patched, define App\Contracts\SmsSender with a single send(string $to, string $message, ?string $from = null): string method, and have OvhSmsSender implement it using the SDK internally. Everything else in the app depends on the interface, which makes testing trivial and lets you swap providers later without touching call sites. This is the single highest-value pattern for a stale dependency.
Build a Laravel notification channel. The idiomatic integration is a custom channel so SMS goes through the same Notification system as mail and database:
class OvhSmsChannel
{
public function __construct(private SmsSender $sender) {}
public function send($notifiable, Notification $notification): void
{
if (! $to = $notifiable->routeNotificationFor('ovhSms', $notification)) {
return;
}
$this->sender->send($to, $notification->toOvhSms($notifiable)->content);
}
}
Then a notifiable model implements routeNotificationForOvhSms() returning $this->phone, and notifications implement toOvhSms(). This keeps message content in the notification class and phone-number policy on the model.
Always queue sends. SMS is a network call to a third party; never do it in an HTTP request. Make SendSms a job with ShouldQueue, a small $tries (2–3), a $backoff, and a retryUntil shorter than the OVH API token window. Laravel's RateLimiter is useful here: OVH enforces per-account and per-number throughput, and 429s from the API are recoverable if you throttle on your side. Use RateLimiter::attempt('ovh-sms', 60, fn () => $sender->send(...)) inside the job's handle().
Keep the message-building separate from sending. Store templates in lang/en/sms.php or as Blade views, render them into a plain string, then hand the string to the sender. Do not build templates inside the job — you want to unit-test message length and content without touching the SDK.
Normalize phone numbers at the boundary. The SDK does not validate numbers. Use propaganistas/laravel-phone or a small validator to coerce everything to E.164 (+33612345678) before it hits the sender, and reject non-mobile numbers early. Store the normalized form in the users table so routeNotificationForOvhSms is a no-op cast.
Cache sender lists and account metadata. Calls like "list senders" or "get remaining credit" don't need to run per send. Cache them for minutes to hours (Cache::remember('ovh.sms.senders', 3600, ...)) and invalidate after you create a sender.
Multiple instances for multiple services. If you have more than one OVH SMS account (e.g., France and Canada), bind named singletons (ovh-sms.eu, ovh-sms.ca) and inject the one a given notification needs. The credentials are not interchangeable across endpoints — an ovh-eu consumer key will return a signature error on ovh-ca.
Log the OVH job ID on every send. The SDK send call should return an identifier you can use to look up delivery status later. Put it in the same log context as your Laravel job ID so support can trace "customer says no text arrived" end to end. Fire a SmsDispatched event with the ID and let listeners do analytics.
Testing: in phpunit.xml, bind a FakeSmsSender implementation of your interface that records calls. Because this SDK talks to OVH over HTTP internally, Http::fake() will not intercept it — you have to fake at your own boundary. That's another reason the interface matters.
Assume the package is abandoned. No release in ~8 years, license field unspecified (still NOASSERTION), and near-zero dependents. Treat it as a vendored dependency: pin the exact version, do not run composer update blind, and be prepared to fork it into repositories in composer.json if a PHP 8.3 deprecation or a Guzzle 3→7 conflict needs a one-line patch. Check for E_DEPRECATED noise at boot; if it's loud, error_reporting(E_ALL & ~E_DEPRECATED) scoped to the SDK's namespace or a fork is the pragmatic answer.
Consumer key scoping is the #1 support issue. The consumer key is generated once and is fixed to the exact set of routes you approved in the OVH console. If you later add a feature that calls a different endpoint (say, retrieving a job status), you'll get a 403 unless that route was in the original scope. Regenerating means updating the .env in every environment, including production. Decide your full route list up front — send, jobs, job status, sender list — and scope generously within your own account.
Endpoints are strict. ovh-eu, ovh-ca, kimsufi-eu, soyoustart-eu, ovh-us. The endpoint string must match where the app credentials were created. Mixing them gives signature errors that look like bad secrets. Put the endpoint in config, not in code, and log it on startup in non-production.
Catch and classify exceptions. The SDK throws on HTTP failure, and the exception surface is inconsistent across error types. Wrap every call in try/catch; rethrow transient failures (timeouts, 5xx, 429) so the queue retries, and swallow/report() permanent ones (invalid number, insufficient credit, unapproved sender) so the job doesn't retry forever. Never use a bare catch (\Exception) that hides configuration errors.
No idempotency keys. If a queued job times out after OVH accepted the request but before you recorded the job ID, a retry will send a duplicate SMS. Keep retries low, use retryUntil on the job, and for high-value messages record state ("dispatch attempted at X") before the call so you can detect and suppress duplicates.
Encoding and segmentation drive cost. GSM-7 allows 160 characters per segment; a single emoji or accented character outside the basic set flips the whole message to UCS-2 at 70 characters per segment, roughly doubling cost. Strip non-GSM characters or explicitly detect and price-check message length before sending. Don't silently truncate — a truncated URL or OTP code is worse than a failed send.
Sender IDs must be pre-approved. An alphanumeric sender or dedicated number has to be validated on the account before it will deliver. The SDK will happily accept an unapproved sender and the message just vanishes. Validate at config time: on boot (or in a scheduled health check) call the sender-list endpoint and fail loudly if config('services.ovh_sms.sender') isn't in it.
Delivery reports aren't in the SDK. OVH supports an account-level HTTP callback for DLRs, configured in the manager — not via this package. If you need delivery confirmation, expose a webhook route, verify the request originates from OVH (shared-secret header or IP allowlist), and update your sms_logs table. Don't try to poll job status per message at volume; OVH rate-limits that too.
config:cache only captures scalars. Everything in services.ovh_sms should be an env() string, not a closure or an object. If you build the SmsApi at config time rather than in a provider, config:cache will break in production only — a classic first-deploy failure.
Watch the SDK's own timeout. If the package's HTTP client has a long default timeout, your queue worker's --timeout will kill the process mid-request and leave you unsure whether the SMS went out. Set the worker timeout comfortably above the SDK's, or fork the SDK to expose a timeout setting.
Consider ovh/php-ovh as a fallback. The general OVH SDK is the maintained successor for the same platform and exposes the /sms routes. If this package becomes unworkable on a future PHP version, your SmsSender interface lets you swap the implementation to the general SDK (or to a different provider entirely) without touching notification or job code.
How can I help you explore Laravel packages today?