smsgatewayme/client
A PHP client for the SMS Gateway Me API that makes it easy to send SMS messages from Laravel or any PHP application. It provides a simple, expressive interface for integrating SMS notifications and transactional messages into your projects.
This package is a thin, framework-agnostic API client — there is no Laravel service provider, facade, or config file. You wire it yourself. That's actually the fastest way to get productive with it.
Step 1 — Install
composer require smsgatewayme/client
Step 2 — Get credentials from smsgate.me
Log into your smsgate.me dashboard, install the Android app on a device (that's how the gateway physically sends SMS), and grab the API token from the Account → API Tokens screen. Note the numeric device ID of each phone — you'll need it on every send.
Step 3 — Register config + a singleton
In config/services.php:
'smsgatewayme' => [
'api_key' => env('SMS_GATEWAY_ME_API_KEY'),
'device_id' => env('SMS_GATEWAY_ME_DEVICE_ID'),
],
In AppServiceProvider::register():
use SMSGatewayMe\Client\ApiClient;
use SMSGatewayMe\Client\Api\MessageApi;
use SMSGatewayMe\Client\Configuration;
$this->app->singleton(MessageApi::class, function () {
$config = Configuration::getDefaultConfiguration();
$config->setApiKey('Authorization', config('services.smsgatewayme.api_key'));
return new MessageApi(new ApiClient($config));
});
Step 4 — First send
use SMSGatewayMe\Client\Model\SendMessageRequest;
$request = new SendMessageRequest();
$request->setPhoneNumber('+447700900123');
$request->setMessage('Hello from Laravel');
$request->setDeviceId((int) config('services.smsgatewayme.device_id'));
$response = app(\SMSGatewayMe\Client\Api\MessageApi::class)->sendMessage($request);
// $response is a \SMSGatewayMe\Client\Model\Message — ->getId(), ->getStatus()
Where to look first: the src/Model/ directory. SendMessageRequest, Message, and Device are the entire vocabulary you'll use day-to-day. MessageApi::searchMessages() is the second thing to learn — it's how you build delivery reports.
If you want one abstraction, wrap MessageApi in a small SmsSender class that accepts a phone number and message string. Laravel developers should treat this package as a transport adapter, not a service.
Queue the send — always. SMS Gateway Me calls are synchronous HTTP requests against a third-party. Every send should go through a ShouldQueue job:
class SendSmsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30;
public function __construct(
public string $phone,
public string $body,
public int $deviceId,
) {}
public function handle(MessageApi $api): void
{
$req = (new SendMessageRequest())
->setPhoneNumber($this->phone)
->setMessage($this->body)
->setDeviceId($this->deviceId);
$api->sendMessage($req);
}
}
Keep the tries low. Duplicate SMS is worse than a dropped notification — the API has no idempotency key.
Multi-device load balancing. If you own several Android devices, DeviceApi::searchDevices() returns their status. A common pattern is a DevicePool class that round-robins across online devices and falls back to the primary from config. Filter on getStatus() === 'online' before selecting — an offline device just fails silently otherwise.
Delivery receipts via polling. There is no native webhook support in this package. You get delivery status by polling searchMessages():
$results = $messageApi->searchMessages(['id' => $messageId]);
$status = $results->getResults()[0]->getStatus(); // Pending → Sent → Delivered → Failed
For anything user-facing, schedule a CheckSmsStatus job every 30–60 seconds for a few minutes rather than keeping the request open. Status is best-effort — the phone must be reachable.
Bulk sends. Loop and dispatch one job per message. Do not pass an array of messages into one call — the API is single-recipient. Space jobs out with ->delay() to stay under the per-device rate limit (roughly 1 msg/sec on the free tier; a queue worker with concurrency 1 against one device is a safe default).
Testing. The client hits a hard-coded base URL, so intercept HTTP in tests with Http::fake() is not enough — the underlying client uses Guzzle directly. Bind a fake MessageApi in the container for feature tests, and cover the real client only in a single integration test that's skipped by default.
Domain wrapper. Keep phone numbers and message templates out of the transport layer. A Notification via Laravel's notification channel is the idiomatic route — implement toSmsGatewayMe($notifiable) on your notification classes and a single SmsGatewayMeChannel that dispatches the job. This keeps your app code unaware of the client's object graph (SendMessageRequest setters) and makes swapping providers a one-file change.
It's essentially unmaintained. Last release 2018, 25 stars, zero known dependents. The API surface (smsgateway.me/api/v4) still works, but there is no active bug fixing. Pin the version and treat it as a fixed appliance — don't expect PHP-8.x-specific deprecation fixes to land. It runs fine on PHP 8.1+ in practice, but the Guzzle dependency constraints may force you onto an older Guzzle alongside a modern Laravel. Check composer why guzzlehttp/guzzle before installing into a big app.
Configuration::getDefaultConfiguration() returns a shared instance. It's a static singleton in practice. Calling setApiKey in a long-running worker (Octane, Queue) once is fine, but if you ever rotate keys at runtime, re-fetch the configuration — otherwise you'll send with a stale token and get silent 401s wrapped in a generic exception.
The API key is a bearer token, not a URL param. The naming (setApiKey('Authorization', ...)) trips people up. You pass the token as the second argument and the header name as the first, exactly as shown in the README. Getting this wrong yields 401 Unauthorized with no detail.
Phone numbers need E.164. Use +447700900123, not 07700900123. The API does not normalise. Validate before dispatch — a bad number looks like a successful send and never arrives.
Device ID is required and per-send. Forgetting to set it throws a validation exception from the model, not from the API. Set it once in a wrapper and stop thinking about it.
Status values are strings, not enums. 'Pending', 'Sent', 'Delivered', 'Failed'. Compare case-sensitively and always have a fallback branch — the gateway occasionally returns status strings not in the docs.
Exceptions are Guzzle-flavoured. A failed send surfaces as a \GuzzleHttp\Exception\ClientException or \SMSGatewayMe\Client\Exception\... depending on where it broke. Wrap the sendMessage call in your job and rethrow a domain exception (SmsSendException) so your failed-jobs table is readable. Log the response body — the client often swallows the useful error detail into the getResponse()->getBody() of the Guzzle exception.
Offline devices fail silently. If your Android phone loses signal, the API still accepts the message and returns Pending, then the message may never transition. Build alerting on "Pending for >10 minutes" rather than trusting the initial 202. This is the single biggest operational surprise in production.
Rate limiting is real and undocumented in the client. Around one message per second per device on most plans. If you push harder, messages get dropped without error. Cap your queue concurrency for SMS jobs at 1 per device, or introduce a RateLimiter::for('sms-device-'.$deviceId, fn ($job) => Limit::perSecond(1)).
Extend via composition, not inheritance. The Api classes are thin wrappers over Guzzle. If you need retries, custom timeouts, or logging, wrap the ApiClient with your own Guzzle handler stack rather than subclassing — the classes aren't designed for it, and a single well-placed decorator (class RetryingMessageApi implements MessageApiInterface) survives package updates far better than overriding methods.
Watch out for the missing interface. Not every Api class has an interface to type-hint against — check src/Api/ before you plan on dependency-injecting it everywhere. When in doubt, type-hint the concrete class and mock it with Mockery in tests.
How can I help you explore Laravel packages today?