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

Verify By Phone Laravel Package

worksome/verify-by-phone

Verify phone numbers in Laravel with one-time codes delivered by SMS or automated phone call. This package offers configurable drivers, rate limiting, and simple model integration, so you can add secure phone verification to registration, login, or profile flows in minutes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish. composer require worksome/verify-by-phone, then publish the config and migration. If you don't remember the tag names, the provider form always works:

php artisan vendor:publish --provider="Worksome\VerifyByPhone\VerifyByPhoneServiceProvider"
php artisan migrate

Where to look first, in this order:

  1. config/verify-by-phone.php — driver selection and credentials. The default driver is Twilio, so you'll need TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_FROM in .env before anything works.
  2. The published migration — it creates a phone_verifications table that maps phone number → code hash → expiry. Read it; it tells you more about the package's behaviour (TTL, uniqueness, indexes) than the README does.
  3. Worksome\VerifyByPhone\Contracts\PhoneVerificationInterface — the two-method contract (send/verify) that everything else in the package funnels through. Once you've read it, you know the whole surface area.

First use case — the two-endpoint OTP flow. Resolve the contract (or use the VerifyByPhone facade) from a controller pair: one endpoint calls send($phoneNumber), the other calls verify($phoneNumber, $code) and returns a boolean. That's the entire integration; everything else in this assessment is hardening around it.

About v0.6.1. This release is Laravel 13.x compatibility (PR #46) plus a CI dev-dependency bump (ramsey/composer-install, irrelevant to consumers). No API changes, no deprecations, no migration changes. If you're on Laravel 10–12, upgrading is a no-op; if you're planning a Laravel 13 upgrade, this is the version that unblocks you. Verify the constraint after upgrading with composer show worksome/verify-by-phone — if you're still on 0.6.0 you'll get a framework conflict.

Implementation Patterns

Split send and verify into separate requests, not one form. Users need to leave your app to receive the SMS; a single-submit flow forces an awkward re-render. Two routes (POST /phone/verify/send, POST /phone/verify/confirm) also gives you a natural place to attach separate rate limiters.

Throttle before you send, not after. SMS costs real money and Twilio will happily bill you for a fuzzing attack. Use Laravel's RateLimiter keyed on both user ID and phone number, and apply it as middleware so it runs before the controller resolves the driver:

RateLimiter::for('phone-send', fn (Request $r) => [
    Limit::perMinute(1)->by($r->user()->id),
    Limit::perHour(5)->by($r->user()->id),
]);

Normalise phone numbers at the boundary. The package stores and compares the string you give it. Do the E.164 conversion once, in a FormRequest::prepareForValidation() or a custom cast, so send() and verify() are guaranteed to receive the same canonical value. Pair this with a PhoneNumber value object if you verify numbers in more than one place.

Gate routes with the shipped middleware. Once a user is verified, the package's verified.phone middleware alias lets you do ->middleware(['auth', 'verified.phone']) on any route group. Register the alias in bootstrap/app.php (Laravel 11+) or Kernel.php if your install predates auto-discovery. This is the cleanest way to keep unverified users out of the app without scattering if (! $user->hasVerifiedPhone()) checks through controllers.

Bind your own driver for non-Twilio providers. The config's driver key resolves through the container, so implementing PhoneVerificationInterface and pointing the config at your binding lets you swap in Vonage, MessageBird, or an internal SMS gateway without touching call sites. This is also the seam for local development — bind a LogPhoneVerification driver in local that writes the code to storage/logs/laravel.log instead of sending it.

Swap the contract in tests. There's no built-in faking helper, so use the container: $this->swap(PhoneVerificationInterface::class, $fake) in a test case, or bind a spy in a service provider for the testing environment. Assert on send/verify invocations rather than hitting Twilio. Keep one integration test that hits the real driver behind a @group external tag so you catch credential rot.

Queue the send when the flow allows it. send() is a synchronous HTTP call to Twilio — typically 100–400ms. If your UX shows "code sent" optimistically, dispatch a job instead. Just make sure the job is on a queue with retries; a swallowed exception here means the user waits for an SMS that never arrives.

Gotchas and Tips

Format mismatches are the number one support ticket. verify('+1 (555) 123-4567', $code) will fail against a code sent to +15551234567. The package does not normalise for you — this is by design, since normalisation is locale-dependent. If you're seeing "correct code rejected," dump both strings before blaming the driver.

env() in the config file is cache-fragile. The Twilio credentials are read via config() at runtime, so php artisan config:cache is fine — but if you've copied the config and inlined env() calls elsewhere in your app, remember those return null once cached. Always config:clear after .env edits in local dev.

Twilio trial accounts silently fail on unverified recipients. Error 21608 ("the number is unverified") only fires for numbers not added to your trial's verified caller list, and error 21408 fires for geo-permissions. Neither is obvious from a generic "verification failed" boolean — log the underlying exception. Consider wrapping the driver call in a try/catch that reports to your error tracker with the Twilio error code attached.

The phone_verifications table grows. Rows aren't automatically reaped unless the underlying model is prunable and you've scheduled model:prune. On a busy app this table becomes the largest thing in your database within months. Add a scheduled cleanup for expired rows, or verify in staging whether the package already marks the model prunable (check for a prunable() method on the verification model before writing your own).

Don't reimplement the comparison. Codes are stored hashed and compared in constant time by the package. If you write a custom driver, preserve that — comparing plaintext codes with == reintroduces a timing side channel and, worse, tempts you to store the code in plaintext.

Attempt limiting is per-row, not per-user. Deleting the verification row on success (or on too many failed attempts) is what resets the counter. If you write a custom driver and skip the delete, a user can burn attempts indefinitely across rows. Also worth knowing: a user who abandons the flow leaves an active row, so a determined attacker with a valid session can hold multiple live codes open at once — cap concurrent sends in your rate limiter as well.

Extension points are narrower than they look. The contract is the only real seam. There are no events to hook for "code sent" / "code verified," so if you need analytics or audit logging, wrap the contract in a decorator and bind that instead of the package implementation. This keeps you upgrade-safe — worth doing before you have five call sites to change.

v0.6.1 and the Laravel 13 upgrade. The compatibility bump is the only consumer-facing change, so the upgrade risk is effectively zero — but the release also confirms the package is actively maintained against new framework majors. If you're evaluating this for a project on Laravel 13, that's the signal you were waiting for. Pin to ^0.6.1 and re-run your test suite; the ramsey/composer-install change only affects the maintainers' CI.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource