Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Client Laravel Package

nexmo/client

Nexmo Client is the legacy PHP SDK for sending SMS, making voice calls, and managing Nexmo/Vonage APIs. It provides simple authentication and request helpers for PHP apps, though new projects should use the maintained Vonage client.

View on GitHub
Deep Wiki
Context7

Getting Started

nexmo/client is a thin metapackage: it requires nexmo/client-core (where every Nexmo\* class actually lives) and a concrete PSR-18/PSR-7 HTTP client so you don't have to wire one yourself. Practically, you install one thing and get Nexmo\Client.

composer require nexmo/client:^2.4

Since 2.4.0 this requires PHP >= 7.2 and Guzzle ^7.0 — check your app's Guzzle constraint before you upgrade an existing install, because Laravel 6/7 skeletons pinned Guzzle 6 and Composer will refuse to resolve until you bump it.

Minimum viable setup — read credentials from config, never hardcode:

use Nexmo\Client;
use Nexmo\Client\Credentials\Basic;

$client = new Client(new Basic(
    config('services.nexmo.key'),
    config('services.nexmo.secret')
));

// Lowest-friction smoke test: outbound SMS
$message = $client->sms()->send(
    new \Nexmo\Message\Text('447700900000', 'Acme', 'Hello from Laravel')
);

$messageId = $message->getMessageId();   // persist this
$message->getStatus();                   // '0' == accepted

Where to look first:

  1. vendor/nexmo/client-core/src/Client.php — the constructor signature and the $options array (http_client, base_api_url, logger) tell you almost everything about integration.
  2. The per-API sub-clients: sms(), messages(), verify(), account(), numbers(), calls()/voice(). Each namespace directory has a Client.php listing the exact method signatures for your pinned version.
  3. vendor/nexmo/client-core/src/Client/Exception/ — four exception types define your entire error-handling strategy.

First real use case: transactional SMS (2FA code or notification) plus the delivery-receipt webhook. That exercises credentials, send, exception handling, and signature verification in one flow — the same surface area you'll reuse for everything else.

Implementation Patterns

Bind it as a singleton in a service provider. The client is stateless and HTTP-connection-heavy; constructing it per call is waste, and in a queued job loop it will dominate your runtime. Because 2.4.0 pins Guzzle 7, the discovered adapter is php-http/guzzle7-adapter (or Guzzle 7 directly, which is PSR-18 compatible) — if you were on guzzle6-adapter, that swap is part of the upgrade.

// AppServiceProvider::register()
$this->app->singleton(Client::class, fn () => new Client(
    new Credentials\Container(
        new Credentials\Basic(
            config('services.nexmo.key'),
            config('services.nexmo.secret')
        ),
        new Credentials\Signature(config('services.nexmo.signature_secret')),
    ),
    ['http_client' => app(\Psr\Http\Client\ClientInterface::class)]
));

The Container matters: Basic (key/secret) covers SMS/Verify/Numbers; Signature (API secret) is needed only for SMS signature-based auth; Keypair (application ID + private key) is needed for Voice and the newer Messages API. Holding all three in one container lets $client->sms(), $client->verify() and $client->calls() each select what they need.

Wrap sending in a small domain service, not controllers. Controllers should not know about Nexmo\Message\Text.

class SmsSender
{
    public function __construct(private Client $client) {}

    public function send(string $toE164, string $text, string $from): string
    {
        try {
            $msg = $this->client->sms()->send(new Text($toE164, $from, $text));
            Log::info('sms.sent', ['id' => $msg->getMessageId(), 'to' => $toE164]);
            return $msg->getMessageId();
        } catch (Exception\Request $e) {
            // 4xx: bad number, unprovisioned sender, insufficient credit
            report($e);
            throw new SmsFailed($e->getMessage(), previous: $e);
        } catch (Exception\Throttle $e) {
            // 429 — retry with backoff, this is recoverable
            throw new SmsThrottled($e->getRateLimit(), previous: $e);
        }
    }
}

Queue the send, always. Wrap SmsSender::send() in a job with $tries = 3, backoff() => [1, 5, 30], and retryUntil — but only retry on Throttle and Server, never on Request. Pass scalars (phone number, text, correlation ID) into the job; never serialize Nexmo\Client or its credentials into the payload.

Idempotency and receipts. The send response gives you message-id but not delivery status — that arrives later as an inbound webhook (status, err-code, message-timestamp). Persist message_id, to, status at send time, keyed uniquely on message_id, then upsert on webhook receipt. Webhooks can be delivered more than once; make the handler idempotent.

Verify (2FA) as a two-route flow. verify()->start() returns a request ID; store it on the user/session row, then verify()->check($requestId, $code) on the confirm route. Don't keep it in the session only — users switch devices mid-flow. Wrap check() separately because it throws a distinct exception on an incorrect code, and you'll want to count attempts yourself before letting Nexmo rate-limit you.

Webhook signature verification. For inbound SMS, verify the sig parameter before trusting the payload:

$verified = $client->verifySignature($request->input('sig'), $request->except('sig'));
abort_unless($verified, 403);

Return 200 fast, dispatch a job for processing, and never call back out to Nexmo inside the webhook handler.

Testing. Inject a mock PSR-18 client instead of hitting the network. php-http/mock-client (Http\Mock\Client) drops straight into the http_client option and lets you assert on the URI and body — note you need a release that's Guzzle-7/PSR-18 aware, so pin it alongside the SDK:

$http = new Http\Mock\Client();
$http->addResponse(new GuzzleHttp\Psr7\Response(200, [], json_encode([...])));
$client = new Client(new Basic('k', 's'), ['http_client' => $http]);

For Laravel feature tests, bind that mocked client into the container in setUp() so nothing real is ever sent from CI.

Gotchas and Tips

  • This package is abandoned. Vonage rebranded the SDK; nexmo/client stopped at 2020-09-10 and was superseded by vonage/client, where Nexmo\Client became Vonage\Client (a mostly mechanical namespace swap, with the sms()/messages() split tidied up). 2.4.0 is the last maintenance bump on that dead branch — a dependency refresh, not new work. If you are starting fresh, install vonage/client; it has current Guzzle support, PHP 8.x fixes, and security patches. Treat this as the thing you're migrating away from, and budget for it.
  • 2.4.0 is a minor version with a breaking dependency change. It raises min PHP to 7.2 and Guzzle to ^7.0 without a major bump, so ^2.0 will silently move you there. If you're on PHP 7.1 or Guzzle 6, resolution fails or you're stuck on 2.3.x. Pin explicitly and commit composer.lock.
  • Version-pin and lock. Because the package is a metapackage, a floating constraint silently pulls a new client-core whose method signatures differ (sms() vs messages(), calls() vs voice(), Text constructor argument order). Pin ^2.4 and commit composer.lock, then read the sub-client's Client.php for your exact version rather than the online docs.
  • Don't composer require nexmo/client-core alongside it. You'll get two copies or a version fight. Let the metapackage resolve it.
  • HTTP client auto-resolution is the most common silent failure. If your app already binds a PSR-18 client (or an adapter is discovered that can't do the request), you get opaque transport errors. Always pass ['http_client' => ...] explicitly — and after the Guzzle 7 jump, confirm the bound client isn't still the 6-era adapter.
  • Phone numbers must be E.164 without the leading + — 447700900000, not +44 7700 900000. This trips up every first implementation. Validate and normalize before it reaches the SDK.
  • from must be a sender ID provisioned on your account (or an approved alphanumeric string). An unprovisioned from throws Exception\Request, not a validation error on your side.
  • Catch the specific exceptions. Nexmo\Client\Exception\Request (4xx, has getEntity() with the raw API response — your debugging goldmine), Exception\Server (5xx, retryable), Exception\Throttle (429, retry with backoff), Exception\Validation (local pre-flight failures). A broad catch (\Exception) will swallow a bad Throttle and turn a retryable blip into a lost message.
  • verifySignature() is SMS-specific and MD5-based. It validates the legacy sig query parameter. It does not validate the Authorization: Bearer <JWT> header used by the newer Messages API/Voice webhooks — those need a JWT check against your application's public key. Using the wrong verifier for the wrong product is a classic security hole.
  • Debugging with a PSR-3 logger. new Client($creds, ['logger' => $logger]) logs request/response bodies, which is invaluable — but the signer and credentials can leak into the output. Gate on app()->isLocal() and redact before shipping to a log aggregator.
  • Guzzle 7 changed error semantics (RequestException no longer exposes the response the same way in some paths) and dropped PHP < 7.2.5. If you have your own Guzzle middleware or HandlerStack customization layered under the SDK, re-test it after the 2.4.0 upgrade — the exception surface you intercept has shifted.
  • Rate limits are per-account, not per-connection. 30 req/s for SMS is easy to blow through when a queued job fans out to a batch. Use Bus::batch() with a throttle, or lean on Exception\Throttle plus exponential backoff rather than a hard-coded sleep().
  • Verify costs money per attempt and codes expire. Don't auto-retry verify()->start() — one per user action, and enforce a client-side cooldown so a user mashing "resend" doesn't burn credit.
  • base_api_url is the escape hatch. Set it to a local fake for integration tests or for proxying through a corporate egress gateway — but remember it changes for the Voice/Numbers hosts too, which are separate base URLs internally.
  • Extension point: if you need request signing, retries, or metrics, do it as PSR-18 middleware on the injected client rather than subclassing Nexmo\Client. The SDK does not expose a middleware stack of its own, and subclassing its HTTP handling breaks across patch releases.
  • Serialization trap: Nexmo\Client and its credential objects are not safe to serialize into a queued job payload — they hold a live HTTP client. Pass IDs and re-resolve from the container in handle().
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums