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

Twilio Verify Laravel Package

ivansotelo/twilio-verify

A Laravel package that wraps Twilio Verify for phone number and OTP verification. Send verification codes, check user-submitted codes, and integrate Twilio's verification service into your PHP application with a simple, framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and configure

composer require ivansotelo/twilio-verify
php artisan vendor:publish --tag=config   # publisher tag may differ; check README/provider

Then set the three values the package needs in .env:

TWILIO_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_VERIFY_SERVICE_SID=VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The VA... service SID is not the account SID. You create a Verify Service in the Twilio Console under Verify → Services; copy the SID from there.

Where to look first

  1. config/twilio-verify.php after publishing — this is the only place the credential keys are consumed.
  2. src/ for the main service class and the facade (small packages like this usually expose one class with send/verify-style methods).
  3. The README on GitHub for the exact method signatures — with a 5-star package the docs are the contract, and there is no ecosystem of blog posts to fall back on.

First use case: OTP on registration

The minimal loop is send → user enters code → check. Confirm the exact method names against src/; a typical call looks like:

use IvanSotelo\TwilioVerify\Facades\TwilioVerify;

// 1. send
TwilioVerify::send($request->phone); // E.164: +15551234567

// 2. check
$approved = TwilioVerify::verify($request->phone, $request->code);

if ($approved) {
    $user->update(['phone_verified_at' => now()]);
}

Wrap both calls in try/catch from day one — Twilio API failures surface as exceptions, and an uncaught one on the registration path is a hard 500 for the user.

Implementation Patterns

Two-endpoint controller with a form request

Keep sending and checking in separate actions so you can rate-limit them independently:

Route::post('/phone/send',   [PhoneVerificationController::class, 'send'])->middleware('throttle:3,1');
Route::post('/phone/verify', [PhoneVerificationController::class, 'verify'])->middleware('throttle:10,1');

Validate phone with a regex or propaganistas/laravel-phone before it reaches the package — Twilio rejects anything that isn't E.164, and you don't want to spend an API round trip discovering that.

Never store the code

Twilio Verify holds the code server-side; the API never returns it. Your database should only track state: phone, phone_verified_at, and optionally a verification_sent_at timestamp for cooldowns. This is the main reason to use Verify instead of rolling your own OTP table.

Wrap the package in your own service

class PhoneVerifier
{
    public function __construct(private TwilioVerify $client) {}

    public function send(string $phone): void
    {
        Cache::put("otp-sent:{$phone}", true, now()->addMinute());
        $this->client->send($phone);
    }
}

This is the single highest-value pattern here. It (a) gives you one place to swap to the official twilio/sdk if the package stalls, (b) makes it trivially fakeable in tests, and (c) centralises your cooldown logic.

Queue the send, verify synchronously

Sending an SMS is a 300–800 ms network call. Push it to a job so registration stays snappy, but keep the verify call inline — the user is waiting on that result.

SendPhoneVerification::dispatch($user->phone)->onQueue('notifications');

Login / step-up flows

The same two calls cover passwordless login and sensitive-action re-confirmation (changing email, withdrawing funds). Model it as a small state machine on the session rather than inventing a second table:

$request->session()->put('pending_verification', [
    'phone' => $phone, 'purpose' => 'login', 'expires_at' => now()->addMinutes(10),
]);

Channel selection

Verify supports sms, call, whatsapp, and email depending on service config. Expose the channel as a parameter and let users pick "text me" vs "call me" — it's a one-line change and a meaningful accessibility win.

Testing

Bind a fake implementing your PhoneVerifier contract in TestCase::setUp(). Avoid hitting Twilio in CI entirely; if you must test the package integration, use Http::fake() on verify.twilio.com and assert the outgoing payload shape.

Gotchas and Tips

The package is a thin, low-traffic wrapper — treat it as such. Five stars, zero dependents, and a last release in August 2024 means no community buffer. Pin the exact version in composer.json, read src/ before relying on a method, and be prepared to fork. The underlying twilio/sdk is a two-method integration if you ever need to cut out the middle layer.

Trial accounts bite. Unverified Twilio trial accounts can only send to numbers you've explicitly verified in the console, and they prepend a "sent from your Twilio trial account" prefix. Budget an hour of "why is nothing arriving" before realising this.

E.164 or nothing. 5551234567, (555) 123-4567, and 05551234567 all fail. Normalise to +15551234567 at the boundary.

Know the error codes you'll actually hit:

Code Meaning What to do
60200 Invalid parameter (bad To) Validate phone format first
20404 Resource not found Wrong Service SID or deleted service
60202 Max check attempts reached Force a resend, don't let the user keep guessing
60203 Max send attempts reached Your own cooldown should have caught this
20003 Authentication failed Wrong SID/token, or token rotated

Catch Twilio\Exceptions\RestException and map getCode() to user-facing copy rather than leaking Twilio's raw message.

Double-check the status, not just the boolean. A verification can be pending, approved, or canceled. If the package returns a raw payload, assert status === 'approved' explicitly — a truthy response object is not approval.

Add your own cooldown. Twilio enforces per-service rate limits, but every rejected send still costs you a request and possibly a fee. A 60-second cache key per phone number prevents the "user mashes resend" case cheaply.

php artisan config:cache in production. If you cache config and then change .env, the stale Service SID will send you on a long debugging detour. php artisan config:clear is the first thing to try when verification mysteriously stops working after a deploy.

Set a timeout. The default Guzzle timeout is effectively "forever" in the worst case. If the package doesn't expose an HTTP client option, wrap the call in your own Http::timeout()-style guard or run the send on a queue so a hanging request can't block a web worker.

Localisation is free. Twilio Verify localises the SMS body based on the destination country, and supports an explicit locale parameter for the voice channel. You get multi-language OTP messages without writing translation files — don't reinvent it.

Cost awareness. Verify bills per successful verification plus the SMS fee. On a low-traffic app this is noise; on a signup flow with retries it adds up. Your resend cooldown is also a cost control.

Extension point worth knowing: because the package resolves its client from the container, you can typically $this->app->bind(...) or instance(...) a stub in a service provider for local development — no real SMS, no real cost, no trial-account friction.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package