padosoft/laravel-rebel-channel-vonage

Vonage provider for Laravel Rebel Channels: phone verification via Vonage Verify (SMS/voice), plain SMS delivery, and signed delivery-receipt webhooks. Part of padosoft/laravel-rebel-*.

Maintainers

Package info

github.com/padosoft/laravel-rebel-channel-vonage

pkg:composer/padosoft/laravel-rebel-channel-vonage

Statistics

Installs: 1

Dependents: 1

Suggesters: 1

Stars: 0

Open Issues: 0

v0.1.0 2026-06-04 00:04 UTC

This package is auto-updated.

Last update: 2026-06-04 00:05:16 UTC


README

Send phone verifications and SMS through Vonage, the Rebel way. This package plugs Vonage Verify (SMS / voice) and the Vonage SMS API into laravel-rebel-channels as a VerificationProvider and a MessageDeliveryChannel — so you get Vonage's global delivery plus Rebel's fraud guard, rate limiting, fallback and audit on top. Part of the padosoft/laravel-rebel-* suite.

Laravel Rebel

Laravel 12|13 PHP 8.3+ PHPStan max Pest 4 Vonage MIT

Table of contents

What it is

A thin, well-tested Vonage provider for Rebel Channels. You don't call it directly — you call the Channels VerificationRouter, and it routes through this provider (with Rebel's bot gate, IRSF defences, per-number rate limit, fallback and audit around it).

It speaks two Vonage APIs:

  • Vonage Verify — start + check one-time codes over SMS / voice. Vonage generates, sends and validates the OTP; you never store or generate the code yourself.
  • Vonage SMS API — send a plain SMS (e.g. a transactional notification) as a MessageDeliveryChannel.

A small gateway seam (VonageGateway) wraps the HTTP calls, so the whole thing is unit-testable offline and has a real live test-suite for the actual API.

Depends on padosoft/laravel-rebel-core and padosoft/laravel-rebel-channels.

            ┌─────────────────────────────────────────────┐
 your app   │  Channels VerificationRouter                │
   │        │   ├─ bot gate · IRSF guard · rate limit      │
   └───────▶│   ├─ fallback order  ─────────┐              │
            │   └─ HMAC'd audit trail        ▼             │
            │                       VonageVerifyProvider   │
            └───────────────────────────────┬─────────────┘
                                             │  VonageGateway (seam)
                                             ▼
                              Verify /verify/json · /verify/check/json
                              SMS    /sms/json
                                             │
                       delivery receipt ◀────┘  POST /rebel/vonage/status
                       (delivered / failed / cost) → audit event

Quick glossary

Term In plain words
Vonage Verify A Vonage product that sends and checks one-time codes for you — you never store or generate the OTP.
API key / API secret Your Vonage account credentials, shown on the dashboard home page.
request_id The handle Vonage returns when you start a verification; you pass it back to check the code.
Brand The sender name (≤18 alphanumeric chars) shown in the Verify message body.
DLR Delivery receipt — Vonage's server-to-server callback telling you whether an SMS was delivered, and what it cost.
Channel sms or voice.

Why this package

What In short
★★★ Vonage Verify, fully wrapped Start + check codes over SMS/voice; you never handle the OTP yourself.
★★★ Rebel guarantees for free Inherits the Channels fraud guard (IRSF), rate limit, fallback and HMAC'd audit.
★★ SMS delivery too Doubles as a MessageDeliveryChannel for plain transactional SMS via the Vonage SMS API.
★★ Never throws out Any transport error becomes a clean provider_error, so the router can fall back to another provider.
★★ Offline-testable A gateway seam + fake means your tests don't hit Vonage; a separate live suite does.
★★ Real delivery receipts A signed DLR webhook records delivered/failed + cost so the panel shows real numbers.
Safe by default No credentials → nothing registers, and no client is ever built with empty keys.

Rebel + Vonage vs the alternatives

Sending an OTP with Vonage, four ways:

Capability Rebel + this package Shopify Vonage Verify SDK (direct) Raw Vonage SMS + your own OTP
Send/check a code via Vonage ➖ (you build OTP logic)
You never store/generate the OTP
Anti toll-fraud / IRSF guard
Per-number rate limit + bot gate
Provider fallback to another vendor
Signed, phone-bound reference (anti replay)
Unified audit trail (number HMAC'd)
Delivery receipts → delivered-rate + cost in panel
Graceful failure → router fallback

Legend: ✅ built-in · ➖ partial / hosted-only · ❌ not available. Vonage is excellent at delivery; this package keeps all of that and adds the Rebel fraud/routing/audit layer around it. Shopify is a closed, hosted commerce platform: it sends its own customer OTPs but lets you neither pick Vonage as the sender, self-host it, fall back across vendors, nor configure its fraud controls — a black box, not a developer-facing verification library.

Vonage dashboard setup (step by step)

  1. Create a Vonage account at dashboard.nexmo.com/sign-up. The free trial gives you a small credit — note that trial accounts can only send to whitelisted test numbers (add your test phone under Account → Test numbers).
  2. Grab your credentials: on the dashboard home page copy your API key and API secret (top of the page).
  3. (SMS sends only) decide your sender ID — an alphanumeric name (e.g. RebelApp, where alphanumeric senders are allowed) or a Vonage virtual number you rent under Numbers → Buy numbers. Put it in VONAGE_SMS_FROM.
  4. (Delivery receipts) set the Delivery Receipts webhook URL (under Account → API settings → SMS settings, or per-application) to https://<your-host>/rebel/vonage/status (see below).
  5. Put the values in your .env (see below). Done — the provider auto-registers.

Pricing: Vonage Verify is billed per successful verification + the channel cost; SMS is billed per segment. The free trial credit is enough to test end-to-end. Always keep the Rebel geo allowlist (in laravel-rebel-channels) tight to avoid IRSF charges.

Installation

composer require padosoft/laravel-rebel-channel-vonage
php artisan vendor:publish --tag="rebel-channel-vonage-config"

Add your credentials to .env:

VONAGE_API_KEY=xxxxxxxx
VONAGE_API_SECRET=xxxxxxxxxxxxxxxx
VONAGE_BRAND=Verify
VONAGE_SMS_FROM=RebelApp

That's it — the provider registers itself into the Channels router under the key vonage.

Configuration

File config/rebel-channel-vonage.php:

Key Default What it does
api_key env(VONAGE_API_KEY) Vonage API key.
api_secret env(VONAGE_API_SECRET) Vonage API secret.
brand env(VONAGE_BRAND, 'Verify') Verify sender name (≤18 alphanumeric chars).
sms_from env(VONAGE_SMS_FROM) Sender ID for plain SMS sends.
channels ['sms','voice'] Which Rebel channels this provider may handle.
register_provider true Auto-register into the Channels registry (when credentials exist).
webhook.enabled true Register the delivery-receipt callback endpoint.
webhook.validate_signature false Validate the Vonage sig on the webhook (enable once you turn on request signing).
webhook.signature_secret env(VONAGE_SIGNATURE_SECRET) The signature secret from the dashboard.
webhook.signature_method md5hash Signature method: md5hash (default) or HMAC md5/sha1/sha256/sha512.
webhook.path rebel/vonage/status The webhook route path.

Usage

Verification (the common case)

You typically don't touch this package directly — you use the Channels router:

use Padosoft\Rebel\Channels\Enums\Channel;
use Padosoft\Rebel\Channels\Routing\VerificationRouter;
use Padosoft\Rebel\Core\Context\SecurityContext;
use Padosoft\Rebel\Core\Contracts\KeyedHasher;
use Padosoft\Rebel\Core\Identifiers\PhoneIdentifier;

$router = app(VerificationRouter::class);
$ctx = SecurityContext::fromRequest($request, app(KeyedHasher::class));

// Send a code (Vonage Verify delivers it)
$start = $router->start(PhoneIdentifier::from('+39 333 1234567'), Channel::Sms, $ctx);

// Keep $start->reference (the Vonage request_id) — you pass it back on check.
$reference = $start->reference;

// Check what the user typed
$result = $router->check(PhoneIdentifier::from('+39 333 1234567'), $request->string('code'), $reference, $ctx);
if ($result->approved()) {
    // verified!
}

Note: Vonage checks the code against the request_id returned by start()not the phone number. Always store and pass back $start->reference. (Without it, check() fails closed with missing_reference.)

To force Vonage specifically, set it first in the Channels fallback order:

// config/rebel-channels.php
'providers' => ['vonage'],

Plain SMS delivery

The provider also implements MessageDeliveryChannel, so you can send a transactional SMS:

use Padosoft\Rebel\Channels\Contracts\MessageDeliveryChannel;
use Padosoft\Rebel\Channels\Enums\Channel;
use Padosoft\Rebel\Channels\Routing\ProviderRegistry;

$vonage = app(ProviderRegistry::class)->get('vonage');

if ($vonage instanceof MessageDeliveryChannel) {
    $result = $vonage->send(
        PhoneIdentifier::from('+39 333 1234567'),
        'Your order has shipped!',
        Channel::Sms,
        $ctx,
    );

    $result->accepted(); // true when Vonage accepted it for delivery
}

Delivery receipts (status webhook)

Vonage knows whether an SMS was actually delivered, failed, and exactly how much it cost — but only it knows, unless you let it call you back. This package ships a delivery-receipt (DLR) webhook that turns those callbacks into Rebel audit events, so the admin panel's Channel Performance can show real delivered / failed rates and spend per channel.

What it records. On each callback the endpoint writes one audit event (recipient number stored only as a keyed HMAC, never in clear):

Vonage status Audit event_type
delivered channel.verification.delivered
failed, rejected, expired, undeliverable channel.verification.undelivered
accepted, buffered, unknown, … channel.verification.dispatched

Each event carries channel: 'sms', provider: 'vonage', the HMAC'd recipient, and a metadata object:

{
  "message_status": "delivered",
  "price": 0.0333,
  "price_unit": null,
  "message_sid": "0A00000123ABCD01",
  "error_code": "0"
}

It accepts both DLR shapes Vonage uses:

  • the classic SMS-API DLR (msisdn, messageId, err-code, price), and
  • the newer Messages-API status callback (to, message_uuid, usage.price, usage.currency).

(Vonage quotes price as a positive string; we store the absolute value so spend sums cleanly, and to stay consistent with the other Rebel channel providers. price/error_code are null when absent.)

Set the webhook URL. In the Vonage dashboard, point the Delivery Receipts callback at your app:

https://<your-host>/rebel/vonage/status

The endpoint is enabled by default (REBEL_VONAGE_WEBHOOK=true); set it to false to drop the route. The path is configurable via webhook.path.

Signature validation. The route has no auth middleware — Vonage posts server-to-server. Vonage only signs callbacks when you enable request signing on the account; once you do, set webhook.validate_signature=true and provide the signature_secret (and the matching signature_method). Every request must then carry a valid sig (recomputed from the sorted params

  • your secret); a missing or forged signature is rejected with 403 and nothing is recorded. A malformed or empty payload is acknowledged with 204 and recorded as nothing, so a junk callback never 500s.
REBEL_VONAGE_WEBHOOK=true             # register the /rebel/vonage/status route
REBEL_VONAGE_WEBHOOK_VALIDATE=false   # set true once request signing is on
VONAGE_SIGNATURE_SECRET=              # the signature secret from the dashboard
VONAGE_SIGNATURE_METHOD=md5hash       # md5hash | md5 | sha1 | sha256 | sha512
REBEL_VONAGE_WEBHOOK_PATH=rebel/vonage/status

Build now, wire live later: the endpoint and its audit events exist today, so you can simulate Vonage callbacks (the test suite does exactly that) and the admin aggregates real delivered/cost data as soon as you set the DLR URL in Vonage.

Live tests against the real API

The offline suite uses a fake gateway. To exercise the real Vonage Verify API (tests/Live), opt in explicitly — it sends a real message:

# .env (or shell env)
REBEL_VONAGE_LIVE=1
VONAGE_TEST_PHONE=+39XXXXXXXXXX   # a whitelisted test number on trial accounts

vendor/bin/pest --group=live

Without REBEL_VONAGE_LIVE=1 or with any credential missing, the live tests self-skip, so composer test and external PRs never trigger a send. In CI, supply the values as secrets and set REBEL_VONAGE_LIVE=1 on a dedicated job.

.env.example

VONAGE_API_KEY=xxxxxxxx
VONAGE_API_SECRET=xxxxxxxxxxxxxxxx
VONAGE_BRAND=Verify
VONAGE_SMS_FROM=RebelApp
REBEL_VONAGE_REGISTER=true

REBEL_VONAGE_WEBHOOK=true
REBEL_VONAGE_WEBHOOK_VALIDATE=false
VONAGE_SIGNATURE_SECRET=
VONAGE_SIGNATURE_METHOD=md5hash
REBEL_VONAGE_WEBHOOK_PATH=rebel/vonage/status

# Live tests (opt-in: SENDS A REAL MESSAGE)
REBEL_VONAGE_LIVE=0
VONAGE_TEST_PHONE=+391234567890

Security notes

  • No client with empty keys: the Vonage gateway is only constructed when both the API key and secret are present.
  • No exception leakage: transport errors are caught and returned as a generic provider_error — Vonage internals never bubble up to your app or logs.
  • Explicit status mapping: only Vonage's 0 is treated as success; any unexpected status fails closed (no bogus "pending" with an empty reference).
  • No PII in clear: the recipient number in delivery receipts is stored only as a keyed HMAC; OTPs and secrets are never logged.
  • Keep IRSF defences on: pair this with the Channels geo allowlist / per-prefix cap to avoid premium-rate fraud charges.

🔋 Vibe coding with batteries included

This package ships AI batteries — so you (and your AI agent) can extend it correctly on the first try:

  • CLAUDE.md — a concise AI working guide (purpose, conventions, architecture, how to extend, Definition of Done). Plain Markdown, so Claude Code, Cursor, Copilot and Codex all read it.
  • AGENTS.md — the agent/workflow contract (branch → PR → CI → tag/release, the gates).
  • .claude/skills/ — invocable skills (at least rebel-package-dev) encoding the suite's TDD loop, the PHPStan-level-max recipes, the security/telemetry rules, and the release discipline.

Open the repo in your AI editor and just start — the rules, guardrails and extension recipes come with it. PRs that follow the shipped CLAUDE.md pass CI (PHPStan max + Pest + Pint) and review the first time around.

Testing & License

composer test      # Pest (provider + gateway + webhook + Channels integration; live suite self-skips)
composer phpstan   # static analysis, level max
composer pint      # code style

License: MIT — see LICENSE. Part of the padosoft/laravel-rebel suite.