SMS Campaign Hooks
FluentCRM Pro IntermediateThese action hooks fire during SMS campaign lifecycle events, message sending, delivery tracking, and subscriber opt-in/out. All SMS hooks require FluentCRM Pro.
Provider Registration
fluent_crm/register_sms_providers
Fires once while the SMS module boots, after the built-in Twilio and AWS drivers are registered. Instantiate your own driver here — AbstractSMSDriver's constructor registers itself with SMSDriverManager, which is what feeds the provider dropdown and settings form.
This runs before the module's isActive() check, so the settings screen always lists your driver even when SMS is not configured yet.
Parameters
None.
Usage:
use FluentCampaign\App\Modules\SMS\Providers\AbstractSMSDriver;
class MySMSDriver extends AbstractSMSDriver
{
public function getSlug(): string
{
return 'my_sms_service';
}
public function getLabel(): string
{
return 'My SMS Service';
}
public function getFields(): array
{
return [
'api_key' => ['type' => 'text', 'label' => 'API Key'],
'api_secret' => ['type' => 'password', 'label' => 'API Secret'],
];
}
public function send(string $to, string $message, array $settings): array
{
// Return ['status' => 'success', 'provider_message_id' => '...'] on success,
// or ['status' => 'error', 'message' => '...'] on failure — the scheduler
// checks $result['status'] === 'success' to mark the message sent.
}
}
add_action('fluent_crm/register_sms_providers', function() {
new MySMSDriver();
});Source: fluentcampaign-pro/app/Modules/SMS/SMSModule.php
fluent_crm/register_whatsapp_providers
WhatsApp counterpart of fluent_crm/register_sms_providers. Fires while the SMS module boots, after the built-in WhatsApp drivers are registered. Instantiate your own driver here — AbstractWhatsAppDriver's constructor registers itself with WhatsAppDriverManager, which is what feeds the WhatsApp provider dropdown and settings form.
WhatsApp channel status
The WhatsApp channel is feature-flagged off in current Pro builds — the do_action() call in SMSModule::register() is commented out and WhatsAppHelper::isActive() returns false — so this hook does not fire yet. It is documented here as the registration point for third-party drivers once the channel is enabled.
Parameters
None.
Usage:
use FluentCampaign\App\Modules\SMS\Providers\AbstractWhatsAppDriver;
class MyWhatsAppDriver extends AbstractWhatsAppDriver
{
public function getSlug(): string
{
return 'my_whatsapp_service';
}
public function getLabel(): string
{
return 'My WhatsApp Service';
}
public function getFields(): array
{
return [
'api_key' => ['type' => 'text', 'label' => 'API Key', 'required' => true, 'default' => ''],
'api_secret' => ['type' => 'password', 'label' => 'API Secret', 'required' => true, 'default' => ''],
];
}
public function send(string $to, string $message, array $settings): array
{
// Return ['status' => 'success', 'status_code' => 200, 'message' => '...',
// 'response' => $response, 'provider_message_id' => '...'] on success,
// or ['status' => 'error', 'message' => '...'] on failure — the scheduler
// checks $result['status'] === 'success' to mark the message sent.
}
}
add_action('fluent_crm/register_whatsapp_providers', function() {
new MyWhatsAppDriver();
});Source: fluentcampaign-pro/app/Modules/SMS/SMSModule.php
Campaign Lifecycle
fluent_crm/sms_campaign_created
Fires when a new SMS campaign is created.
Parameters
$campaignSMSCampaign Model
Usage:
add_action('fluent_crm/sms_campaign_created', function($campaign) {
// New SMS campaign created
});Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
fluent_crm/sms_campaign_updated
Fires when an SMS campaign is updated.
Parameters
$campaignSMSCampaign Model
Usage:
add_action('fluent_crm/sms_campaign_updated', function($campaign) {
// SMS campaign was modified
});Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
fluent_crm/sms_campaign_status_active
Fires at the start of SMSController::schedule(), once the campaign has passed the "must still be a draft" guard but before any status change is written.
WARNING
The campaign passed to this hook still has status = 'draft'. Re-read the model if you need the post-schedule status.
Parameters
$smsCampaignSMSCampaign Model - still indraftstatus at this point
Usage:
add_action('fluent_crm/sms_campaign_status_active', function($smsCampaign) {
// SMS campaign activated
});Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
fluent_crm/sms_campaign_scheduled
Fires when an SMS campaign is scheduled for future sending. It does not fire for a send-immediately campaign — that path schedules the batch-generation job directly instead.
Parameters
$smsCampaignSMSCampaign Model - freshly re-read from the database, so itsstatusandscheduled_atare the saved values$scheduledAtString - the campaign'sscheduled_atcolumn, a site-localY-m-d H:i:sdatetime (not a Unix timestamp)
Usage:
add_action('fluent_crm/sms_campaign_scheduled', function($smsCampaign, $scheduledAt) {
// $scheduledAt is a MySQL datetime string, e.g. '2026-08-12 09:30:00'
$timestamp = strtotime($scheduledAt);
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
fluent_crm/sms_campaign_processing_start
Fires when a pending-scheduled campaign flips to processing — that is, when its scheduled time is less than six minutes away and the admin screen polls for processing stats. The campaign has already been saved with status = 'processing' and recipients_count = 0 when this runs.
Parameters
$campaignSMSCampaign Model - already saved asprocessing
Usage:
add_action('fluent_crm/sms_campaign_processing_start', function($campaign) {
// SMS campaign processing started
});Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
fluent_crm/sms_campaign_duplicated
Fires when an SMS campaign is duplicated.
Parameters
$newCampaignSMSCampaign Model - the new copy, created as adraftwith a[Duplicate]title prefix and the original's labels already attached$oldCampaignSMSCampaign Model - the original
Usage:
add_action('fluent_crm/sms_campaign_duplicated', function($newCampaign, $oldCampaign) {
// SMS campaign was duplicated
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
fluent_crm/sms_campaign_archived
Fires when an SMS campaign is archived — either because a recurring/auto-processing campaign has no future runs left, or by the cleanup cron when a finished campaign has no unsent messages remaining. The campaign row is saved as status = 'archived' in all cases, but only the first two call sites re-read the model before firing; on the cron path the model still carries its pre-archive status.
Parameters
$smsCampaignSMSCampaign Model - the row is saved asarchived, though on the cron path the passed instance may still show the previous status
Usage:
add_action('fluent_crm/sms_campaign_archived', function($smsCampaign) {
// SMS campaign archived
});Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php, fluentcampaign-pro/app/Modules/SMS/SMSScheduler.php
fluent_crm/sms_campaign_deleted
Fires after an SMS campaign is permanently deleted, both from the single-delete endpoint and once per campaign from the delete_campaigns bulk action.
WARNING
The campaign row and its message/meta data are already gone when this fires — only the ID is passed. Capture anything you need on fluent_crm/sms_campaign_updated instead.
Parameters
$campaignIdINT - deleted campaign ID
Usage:
add_action('fluent_crm/sms_campaign_deleted', function($campaignId) {
// SMS campaign deleted
});Source: fluentcampaign-pro/app/Modules/SMS/Http/Controllers/SMSController.php
Sending & Delivery
fluent_crm/sms_sent
Fires after an SMS message is successfully sent, once the message row has been marked sent and the campaign's sent_count incremented.
Parameters
$smsMessageSMSMessage Model - the pre-update instance, so itsstatusstill reflects the value from before the send was recorded$resultArray - the driver's response; carriesprovider_message_idwhen the provider returns one
Usage:
add_action('fluent_crm/sms_sent', function($smsMessage, $result) {
// SMS sent successfully
// $result contains provider-specific response data
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSScheduler.php
fluent_crm/sms_failed
Fires after an SMS message fails to send, once the message row has been marked failed and the campaign's failed_count incremented.
Parameters
$smsMessageSMSMessage Model - the pre-update instance, so itsstatusstill reflects the value from before the failure was recorded$errorMessageString - error message from the provider; also stored on the message'snotescolumn
Usage:
add_action('fluent_crm/sms_failed', function($smsMessage, $errorMessage) {
// SMS failed - log or retry
error_log('SMS failed: ' . $errorMessage);
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSScheduler.php
fluent_crm/whatsapp_sent
WhatsApp counterpart of fluent_crm/sms_sent — messages route here when their channel is whatsapp. Fires after a WhatsApp message is successfully sent, once the message row has been marked sent and the campaign's sent_count incremented. Note it fires in addition to the generic fluent_crm/sms_sent (which runs for every channel), not instead of it — the same applies to whatsapp_failed and sms_failed.
Parameters
$smsMessageSMSMessage Model - the pre-update instance, so itsstatusstill reflects the value from before the send was recorded$resultArray - the WhatsApp driver's response (status,status_code,message,response); carriesprovider_message_idwhen the provider returns one
Usage:
add_action('fluent_crm/whatsapp_sent', function($smsMessage, $result) {
// WhatsApp message sent successfully
// $result contains provider-specific response data
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSScheduler.php
fluent_crm/whatsapp_failed
WhatsApp counterpart of fluent_crm/sms_failed. Fires after a WhatsApp message fails to send, once the message row has been marked failed and the campaign's failed_count incremented. Pre-send guards that mark a message failed without attempting a send — for example a contact whose whatsapp_status is not whatsapp_subscribed — do not fire this hook.
Parameters
$smsMessageSMSMessage Model - the pre-update instance, so itsstatusstill reflects the value from before the failure was recorded$errorMessageString - the driver response'smessage, orUnknown errorwhen it carries none; also stored on the message'snotescolumn
Usage:
add_action('fluent_crm/whatsapp_failed', function($smsMessage, $errorMessage) {
// WhatsApp send failed - log or retry
error_log('WhatsApp failed: ' . $errorMessage);
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSScheduler.php
Opt-in & Opt-out
fluent_crm/contact_sms_subscribed
Fires when an inbound message opts a contact in to SMS. The contact is matched by phone, so nothing fires for an unknown number. The contact's sms_status is already saved as sms_subscribed.
Parameters
$subscriberSubscriber Model - already saved withsms_status = 'sms_subscribed'$dataArray - the inbound webhook context; includes aproviderkey
Usage:
add_action('fluent_crm/contact_sms_subscribed', function($subscriber, $data) {
// Contact opted in to SMS
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSHelper.php
fluent_crm/contact_sms_unsubscribed
Fires when an inbound message opts a contact out of SMS. The contact is matched by phone, so nothing fires for an unknown number. The contact's sms_status is already saved as sms_unsubscribed.
Parameters
$subscriberSubscriber Model - already saved withsms_status = 'sms_unsubscribed'$dataArray - the inbound webhook context; includes aproviderkey
Usage:
add_action('fluent_crm/contact_sms_unsubscribed', function($subscriber, $data) {
// Contact opted out of SMS
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSHelper.php
fluent_crm/contact_whatsapp_subscribed
WhatsApp counterpart of fluent_crm/contact_sms_subscribed. Fires when an inbound WhatsApp message — a start/subscribe keyword arriving on the Twilio WhatsApp or Meta Cloud webhook — opts a contact in to WhatsApp. The contact is matched by phone, so nothing fires for an unknown number. The contact's whatsapp_status is already saved as whatsapp_subscribed.
Parameters
$subscriberSubscriber Model - already saved withwhatsapp_status = 'whatsapp_subscribed'$dataArray - the inbound webhook context; includes aproviderkey (twilio_whatsappormeta_cloud)
Usage:
add_action('fluent_crm/contact_whatsapp_subscribed', function($subscriber, $data) {
// Contact opted in to WhatsApp
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/WhatsAppHelper.php
fluent_crm/contact_whatsapp_unsubscribed
WhatsApp counterpart of fluent_crm/contact_sms_unsubscribed. Fires when an inbound WhatsApp message — a stop/cancel/unsubscribe keyword arriving on the Twilio WhatsApp or Meta Cloud webhook — opts a contact out of WhatsApp. The contact is matched by phone, so nothing fires for an unknown number. The contact's whatsapp_status is already saved as whatsapp_unsubscribed.
Parameters
$subscriberSubscriber Model - already saved withwhatsapp_status = 'whatsapp_unsubscribed'$dataArray - the inbound webhook context; includes aproviderkey (twilio_whatsappormeta_cloud)
Usage:
add_action('fluent_crm/contact_whatsapp_unsubscribed', function($subscriber, $data) {
// Contact opted out of WhatsApp
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/WhatsAppHelper.php
Provider Webhooks
fluent_crm_sms_custom_provider_webhook
Generic webhook hook for custom SMS providers. Fires when an incoming webhook is received for a provider that has no built-in handler. The signature check has already passed and the payload is sanitized by the time this runs.
WARNING
Built-in providers never reach this hook. twilio, twilio_whatsapp and meta_cloud are routed to their own handlers in SMSHandler, so only providers registered through fluent_crm/register_sms_providers fire it.
Parameters
$bodyDataArray - the sanitized webhook request body$providerString - provider slug taken from the webhook URL
Usage:
add_action('fluent_crm_sms_custom_provider_webhook', function($bodyData, $provider) {
if ($provider === 'my_sms_service') {
// Handle delivery receipt, status update, etc.
}
}, 10, 2);Source: fluentcampaign-pro/app/Modules/SMS/SMSReceiver.php
fluent_crm_sms_{$provider}_webhook
Provider-specific webhook hook, fired immediately after fluent_crm_sms_custom_provider_webhook. The hook name includes the provider slug — for a driver whose getSlug() returns my_sms_service, the hook is fluent_crm_sms_my_sms_service_webhook.
WARNING
Like the generic hook above, this only fires for custom providers. There is no fluent_crm_sms_twilio_webhook — Twilio, Twilio WhatsApp and Meta Cloud are handled by their built-in SMSReceiver methods and never reach this dispatcher.
Parameters
$bodyDataArray - the sanitized webhook request body
Usage:
add_action('fluent_crm_sms_my_sms_service_webhook', function($bodyData) {
// Handle a delivery receipt or inbound reply from your own provider
});Source: fluentcampaign-pro/app/Modules/SMS/SMSReceiver.php