glueful / conversa
SMS & WhatsApp messaging channels for Glueful (Twilio, Meta WhatsApp Cloud)
Requires
- php: ^8.3
Requires (Dev)
- glueful/framework: ^1.51.0
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.6
README
๐ Version 0.1.0 โ
sms/
Overview
Conversa is Glueful's phone-based messaging layer โ it registers sms and
whatsapp notification channels and sends through swappable provider backends
(Twilio and Meta WhatsApp Cloud API, with more to come). Applications send
alerts, reminders, order updates, and customer messages without caring which
provider is wired up underneath.
It completes Glueful's multi-channel notification story:
| Extension | Channel(s) | Handles |
|---|---|---|
glueful/email-notification |
email |
Transactional & notification email |
glueful/notiva |
push |
Mobile & web push (FCM, APNs, Web Push) |
glueful/conversa |
sms, whatsapp |
Phone-based messaging |
Conversa provides the delivery means, not workflow logic. Just as
email-notification registers an email channel for the framework's notification
system to use, Conversa registers sms/whatsapp channels โ your application code
decides what to send and when; Conversa delivers it and tracks the result
(see Notification-system integration).
Why phone messaging
- Reach where email/push fall short. In many markets (across Africa, LATAM, and mobile-first products generally) SMS and WhatsApp are far more reliable than email and are not as easily disabled as push.
- Transactional & high-priority alerts. Account and transaction confirmations and other time-sensitive notifications often need a phone-based delivery channel โ Conversa is that channel; your application drives the workflow.
- Provider independence. Run both channels on Twilio (it serves SMS and WhatsApp), or keep WhatsApp on Meta Cloud API while Twilio handles SMS โ and add more providers later behind the same interface, without touching application code.
Features
- โ
smsandwhatsappnotification channels registered with the framework'sChannelManager(the same contractemail-notification'sEmailChannelimplements) - โ Send SMS through a configured driver
- โ Send WhatsApp through a configured driver โ free text or approved templates
- โ Provider drivers behind one interface (Twilio, WhatsApp Cloud, log), selectable per channel via config
- โ
WhatsApp templates โ a logical template name maps to each driver's identity (Twilio
ContentSid, Meta name + language) viatemplatesconfig - โ
Idempotent direct sends โ caller-supplied
Idempotency-Keycollapses retries to one message - โ Delivery webhooks โ receive provider status callbacks (fail-closed signature verification) and update message state
- โ Message logging โ persist every send, its delivery state, provider response, and retries
- โ
Lifecycle events โ
MessageSent,MessageDelivered,MessageFailed
Conversa is the delivery channel, not the workflow. Conversational features (inbound replies, threads, campaigns, contact lists, opt-in management) are tracked on the Roadmap.
Requirements
- PHP 8.3+
- Glueful Framework 1.49.1+
- A configured messaging provider account (Twilio or Meta WhatsApp Cloud API)
- Outbound HTTPS access to the provider's API (calls go through Glueful's HTTP client)
Installation
composer require glueful/conversa # Enable it โ installing does not auto-load an extension; this adds the provider # to config/extensions.php's `enabled` list and recompiles the cache. php glueful extensions:enable conversa # Create the message-log table php glueful migrate run # Verify discovery and channel registration php glueful extensions:list php glueful extensions:info conversa php glueful extensions:diagnose
In production, manage the enabled list in config and run
php glueful extensions:cache in your deploy step (production boots only from the
compiled manifest).
Configuration
Conversa is configured via config/conversa.php (merged from the extension) and
environment variables. Each channel selects a default driver; you can run SMS
and WhatsApp on different providers.
# Channel โ driver selection (default: log โ writes to the log instead of a provider, # so the extension is safe to enable before any credentials are configured) CONVERSA_SMS_DRIVER=twilio # twilio | log CONVERSA_WHATSAPP_DRIVER=whatsapp_cloud # whatsapp_cloud | twilio | log # Message logging / retries CONVERSA_LOG_MESSAGES=true CONVERSA_MAX_RETRIES=3 # Privacy (both default true) CONVERSA_STORE_BODY=true # persist the message body/template vars in the log CONVERSA_REDACT_PROVIDER_RESPONSE=true # redact recipient/body fields from the stored provider response # Public base URL used to rebuild the callback URL Twilio signed when behind a proxy/LB CONVERSA_WEBHOOK_BASE_URL=https://api.example.com
Provider credentials
# Twilio (SMS and/or WhatsApp) CONVERSA_TWILIO_SID=ACxxxxxxxx CONVERSA_TWILIO_TOKEN=xxxxxxxx CONVERSA_TWILIO_SMS_FROM=+15551234567 CONVERSA_TWILIO_WHATSAPP_FROM=whatsapp:+15551234567 # Meta WhatsApp Cloud API CONVERSA_WHATSAPP_PHONE_ID=1234567890 CONVERSA_WHATSAPP_TOKEN=EAAxxxxxxxx CONVERSA_WHATSAPP_VERIFY_TOKEN=your-webhook-verify-token # GET webhook handshake CONVERSA_WHATSAPP_APP_SECRET=xxxxxxxx # webhook signature check
Only the drivers you actually use need credentials. When the driver selected for a
channel is unavailable (missing credentials/sender), the send is recorded as a
failed message and a MessageFailed event is dispatched โ the send path never
crashes, and you can see the failure in the message log.
Provider drivers
Every provider implements one driver interface, so application code never depends on a specific vendor. Conversa ships with:
| Driver key | Provider | SMS | |
|---|---|---|---|
twilio |
Twilio | โ | โ |
whatsapp_cloud |
Meta WhatsApp Cloud API | โ | โ |
log |
Logs the message instead of sending (dev/test default) | โ | โ |
Driver availability is channel-aware: Twilio reports whatsapp as available
only when CONVERSA_TWILIO_WHATSAPP_FROM is set, and sms only when
CONVERSA_TWILIO_SMS_FROM is set โ so a one-channel Twilio setup never advertises
the other channel.
More providers (e.g. Africa's Talking, Vonage) are planned behind the same interface โ see Roadmap.
Switching providers is a config change (CONVERSA_SMS_DRIVER / CONVERSA_WHATSAPP_DRIVER)
โ no code changes. Adding a new provider means implementing the driver interface
and registering it; the channels and public API are unchanged.
Notification-system integration
Conversa plugs into Glueful's notification system the same way email-notification
does. It implements NotificationChannel for sms and whatsapp and registers both
with the ChannelManager during boot, so anything that dispatches to those channels
is delivered by Conversa:
getChannelName()returns the channel (sms/whatsapp).send($notifiable, $data)readsrouteNotificationFor('sms'|'whatsapp')for the destination number and sends$data['body'](or$data['template']).
Any NotificationDispatcher::send(..., ['sms']) (or ['whatsapp']) call โ from your
own code or elsewhere in the framework โ routes through Conversa, which sends via the
configured driver and records the result.
Endpoints
Base prefix: /conversa. Application endpoints require auth and apply rate
limiting; webhook endpoints are public but signature/verify-token protected.
| Method & path | Purpose |
|---|---|
POST /conversa/messages |
Send an SMS or WhatsApp message directly (honours an Idempotency-Key header) |
GET /conversa/messages |
Query the message log, filterable by status / channel / to, paginated via page / per_page |
GET /conversa/webhooks/{provider} |
Provider webhook handshake (e.g. Meta verify token) |
POST /conversa/webhooks/{provider} |
Provider delivery-status callbacks |
API_BASE=http://localhost:8000 TOKEN="<YOUR_BEARER_TOKEN>" # Send an SMS directly curl -s -X POST "$API_BASE/conversa/messages" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "channel": "sms", "to": "+15551234567", "body": "Your order has shipped" }' | jq . # Send a WhatsApp message directly curl -s -X POST "$API_BASE/conversa/messages" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "channel": "whatsapp", "to": "+15551234567", "body": "Welcome to Acme!" }' | jq . # Send a WhatsApp template (mapped to a provider template via `templates` config) curl -s -X POST "$API_BASE/conversa/messages" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "channel": "whatsapp", "to": "+15551234567", "template": { "name": "order_shipped", "variables": ["1Z999"] } }' | jq . # Idempotent send โ repeating the same key returns the original message, no second send curl -s -X POST "$API_BASE/conversa/messages" \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -H "Idempotency-Key: order-4711-shipped" \ -d '{ "channel": "sms", "to": "+15551234567", "body": "Your order has shipped" }' | jq . # Query the message log (filter + paginate) curl -s "$API_BASE/conversa/messages?status=delivered&channel=sms&per_page=25&page=1" \ -H "Authorization: Bearer $TOKEN" | jq .
Usage (PHP)
Via the notification system (preferred)
Because Conversa registers sms and whatsapp channels, any Notifiable that
routes those channels receives phone messages through the same dispatcher used for
email and push:
use Glueful\Notifications\Services\ChannelManager; $sms = container()->get(ChannelManager::class)->getChannel('sms'); $sms->send($user, ['body' => 'Your appointment is tomorrow at 9am']);
use Glueful\Notifications\Contracts\Notifiable; class User implements Notifiable { public function routeNotificationFor(string $channel) { return match ($channel) { 'sms', 'whatsapp' => $this->phone, // E.164, e.g. +15551234567 default => null, }; } // getNotifiableId(), shouldReceiveNotification(), getNotificationPreferences() โฆ }
Direct send (one-off messages)
ConversaService::send() takes the channel, recipient, a payload array (exactly one
of body or template), and optional opts (idempotency_key, from, meta). It
returns a DriverResult (->ok, ->providerMessageId, ->error).
use Glueful\Extensions\Conversa\ConversaService; $conversa = app($context, ConversaService::class); // Free-text SMS / WhatsApp $conversa->send('whatsapp', '+15551234567', ['body' => 'Welcome to Acme!']); // WhatsApp template (resolved to a provider template via `templates` config) $conversa->send('whatsapp', '+15551234567', [ 'template' => ['name' => 'order_shipped', 'variables' => ['1Z999']], ]); // Idempotent send โ a repeat key returns the original message without sending again $result = $conversa->send('sms', '+15551234567', ['body' => 'Your order has shipped'], [ 'idempotency_key' => 'order-4711-shipped', ]); // $result->ok, $result->providerMessageId, $result->error
Delivery tracking
Every send is recorded in a conversa_messages table (created by the extension's
migration), capturing at least:
- recipient, channel, driver, and the message body/template reference
- lifecycle state:
queued โ sent โ delivered(orfailed/undelivered) - the provider's message ID and raw response
- retry count and last error
Delivery webhooks (POST /conversa/webhooks/{provider}) update the stored state
as providers report sent / delivered / failed, giving you an auditable
record for support and reconciliation. The message-log API (GET /conversa/messages)
queries this table.
Webhooks
- Meta WhatsApp Cloud performs a
GEThandshake against/conversa/webhooks/whatsapp_cloudusingCONVERSA_WHATSAPP_VERIFY_TOKEN, and signsPOSTcallbacks withCONVERSA_WHATSAPP_APP_SECRET(verified before processing). - Twilio posts delivery-status callbacks for both SMS and WhatsApp to
/conversa/webhooks/twilio; configure that URL in the Twilio console. Signature verification is applied where the provider supports it.
Point each provider's status-callback / webhook URL at the matching path and Conversa reconciles delivery state automatically.
Roadmap
Conversa today is the sending + tracking core. Planned follow-ups:
- Two-way messaging โ inbound message webhooks, replies, and conversation threads (WhatsApp especially is bidirectional).
- Campaigns & broadcasts โ contact lists, batch sends, scheduling.
- Preferences & compliance โ opt-in/opt-out management, per-recipient channel preferences, quiet hours.
- More drivers โ additional providers behind the same interface (e.g. Africa's Talking, Vonage, and other regional gateways).
Security considerations
- Treat provider tokens/secrets as secrets; never commit them and restrict who can read the
.env. - Verify webhook authenticity (Meta app-secret signature, provider signing where available) before trusting status updates.
- Do not log full message bodies or recipient numbers in production beyond what's needed for support; message bodies may carry sensitive content (see
CONVERSA_STORE_BODY/CONVERSA_REDACT_PROVIDER_RESPONSE). - Apply rate limits to send endpoints (included) to prevent abuse and runaway provider spend.
- Store phone numbers in E.164 and treat them as personal data.
Metadata
- Package:
glueful/conversa(type: glueful-extension) - Provider:
Glueful\Extensions\Conversa\ConversaServiceProvider - Channels:
sms,whatsapp(implementNotificationChannel, registered withChannelManager) - Events:
MessageSent,MessageDelivered,MessageFailed(extend the frameworkBaseEvent) - Config:
config/conversa.phpยท Env prefix:CONVERSA_* - Migration:
conversa_messages
Troubleshooting
- Extension not loading โ installing doesn't enable it; run
php glueful extensions:enable conversa, thenphp glueful extensions:diagnose. In production, runphp glueful extensions:cache. - Channel not found when sending โ confirm the extension is enabled and the
sms/whatsappchannels registered (extensions:diagnose); the framework looks them up by name via theChannelManager. - Sends recorded as
failedwithdriver_unavailableโ the selected driver's credentials/sender are missing; checkextensions:diagnoseand the logs. The send is logged asfailed(and aMessageFailedevent fires) rather than crashing. - WhatsApp webhook 403 on setup โ
CONVERSA_WHATSAPP_VERIFY_TOKENmust match the value entered in the Meta dashboard. - Delivery state stuck at
sentโ the provider's status-callback URL isn't pointed at/conversa/webhooks/{provider}, or signature verification is rejecting it. - No message-log rows โ run migrations (
php glueful migrate run) and ensureCONVERSA_LOG_MESSAGES=true.