# Contact Hooks These action hooks fire during contact lifecycle events — creation, updates, tag/list changes, status transitions, notes, and more. ## Contact Created & Updated ### `fluent_crm/contact_created` This action runs when a new contact is created. ::: warning Suppressed by silent imports Bulk inserts skip this hook entirely when the `FLUENTCRM_DISABLE_TAG_LIST_EVENTS` constant is defined — that is what "import silently" does, so imported rows do not enroll into *Contact Created* automations. Do not rely on this hook to see every row that reaches `fc_subscribers`. The bulk hooks [`fluentcrm_contacts_imported_bulk`](#fluentcrm-contacts-imported-bulk) and [`fluentcrm_contacts_updated_bulk`](#fluentcrm-contacts-updated-bulk) are **not** suppressed — listen on those to see silently imported rows. ::: ::: tip Deprecated alias `fluentcrm_contact_created` fires alongside this hook with the same signature. It has been deprecated since 2.8.0 — use `fluent_crm/contact_created`. ::: **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) **Usage:** ```php add_action('fluent_crm/contact_created', function($subscriber) { // Do whatever you want with the newly created $subscriber }); ``` **Source:** `app/Models/Subscriber.php` --- ### `fluent_crm/contact_updated` This action runs when a contact is updated. It fires from many paths — the model's bulk import and `createOrUpdate()`, the REST controller, the auto-subscribe handler, the manage-subscription preference form, and the Pro *Update Contact Property* automation action. ::: tip Deprecated alias `fluentcrm_contact_updated` fires alongside this hook with the same signature on every path except the auto-subscribe handler, which fires only the `fluent_crm/` name. It has been deprecated since 2.8.0 — use `fluent_crm/contact_updated`. ::: **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - already saved - `$dirtyFields` Array - the changed fields, keyed by column name, holding the **new** values. Use [`fluent_crm/contact_updated_with_changes`](#fluent-crm-contact-updated-with-changes) if you also need the old values. **Usage:** ```php add_action('fluent_crm/contact_updated', function($subscriber, $dirtyFields) { // $dirtyFields contains the changed field values }, 10, 2); ``` **Source:** `app/Models/Subscriber.php`, `app/Http/Controllers/SubscriberController.php`, `app/Hooks/Handlers/AutoSubscribeHandler.php`, `app/Hooks/Handlers/PrefFormHandler.php`, `fluentcampaign-pro/app/Services/Funnel/Actions/UpdateContactPropertyAction.php` --- ### `fluent_crm/contact_updated_with_changes` This action provides detailed change tracking, including both old and new values. Fires when a contact is updated via the admin UI or Fluent Forms. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$dirtyFields` Array - changed fields (or custom field values) - `$oldData` Mixed - original data before changes ([Subscriber Model](/database/models/subscriber) or old custom fields array) - `$meta` Array - context info e.g. `['source' => 'web', 'type' => 'all_fields']` **Usage:** ```php add_action('fluent_crm/contact_updated_with_changes', function($subscriber, $dirtyFields, $oldData, $meta) { if ($meta['type'] === 'custom_fields_only') { // Only custom fields were changed } // Compare $dirtyFields with $oldData for detailed change tracking }, 10, 4); ``` **Source:** `app/Http/Controllers/SubscriberController.php`, `app/Services/ExternalIntegrations/FluentForm/Bootstrap.php` --- ### `fluent_crm/contact_custom_data_updated` This action runs when a contact's custom field values are updated. **Parameters** - `$newValues` Array - new custom field values - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$updateValues` Array - the values that were actually updated **Usage:** ```php add_action('fluent_crm/contact_custom_data_updated', function($newValues, $subscriber, $updateValues) { // React to custom field changes }, 10, 3); ``` **Source:** `app/Models/Subscriber.php` --- ### `fluent_crm/contact_email_changed` This action hook fires when a subscriber's email has been changed to a new email address. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$oldEmail` string - Old Email Address **Usage:** ```php add_action('fluent_crm/contact_email_changed', function($subscriber, $oldEmail) { // the contact's email changed. You can run your code here }, 10, 2); ``` **Source:** `app/Models/Subscriber.php`, `app/Http/Controllers/SubscriberController.php`, `app/Hooks/Handlers/AutoSubscribeHandler.php`, `app/Hooks/Handlers/ExternalPages.php`, `app/Modules/MCP/Tools/ContactTools.php` --- ### `fluent_crm/subscriber_avatar_update` This action fires when a contact's avatar is updated via the admin profile editor. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$oldValue` string - previous avatar URL **Usage:** ```php add_action('fluent_crm/subscriber_avatar_update', function($subscriber, $oldValue) { // Avatar was changed }, 10, 2); ``` **Source:** `app/Http/Controllers/SubscriberController.php` --- ## Tags & Lists Assignment ::: warning Suppressed by silent imports Like `fluent_crm/contact_created`, all four tag/list hooks are skipped when the `FLUENTCRM_DISABLE_TAG_LIST_EVENTS` constant is defined — the CSV importer and the WP-user importer both define it. They also only fire for rows that actually changed, so re-attaching an existing tag or list is silent. ::: ### `fluent_crm/contact_added_to_tags` This action runs when tags have been added to a contact. > **Note:** The legacy hook `fluentcrm_contact_added_to_tags` also fires with reversed parameter order: `($tagIds, $subscriber)`. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$attachedTagIds` Array - tag IDs that were added **Usage:** ```php add_action('fluent_crm/contact_added_to_tags', function($subscriber, $tagIds) { // Do whatever you want here }, 10, 2); ``` **Source:** `app/Models/Subscriber.php` --- ### `fluent_crm/contact_added_to_lists` This action runs when lists have been added to a contact. > **Note:** The legacy hook `fluentcrm_contact_added_to_lists` also fires with reversed parameter order: `($listIds, $subscriber)`. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$attachedListIds` Array - list IDs that were added **Usage:** ```php add_action('fluent_crm/contact_added_to_lists', function($subscriber, $listIds) { // Do whatever you want here }, 10, 2); ``` **Source:** `app/Models/Subscriber.php` --- ### `fluent_crm/contact_removed_from_tags` This action runs when tags have been removed from a contact. > **Note:** The legacy hook `fluentcrm_contact_removed_from_tags` also fires with reversed parameter order: `($tagIds, $subscriber)`. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$detachedTagIds` Array - tag IDs that were removed **Usage:** ```php add_action('fluent_crm/contact_removed_from_tags', function($subscriber, $tagIds) { // Do whatever you want here }, 10, 2); ``` **Source:** `app/Models/Subscriber.php` --- ### `fluent_crm/contact_removed_from_lists` This action runs when lists have been removed from a contact. > **Note:** The legacy hook `fluentcrm_contact_removed_from_lists` also fires with reversed parameter order: `($listIds, $subscriber)`. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$detachedListIds` Array - list IDs that were removed **Usage:** ```php add_action('fluent_crm/contact_removed_from_lists', function($subscriber, $listIds) { // Do whatever you want here }, 10, 2); ``` **Source:** `app/Models/Subscriber.php` --- ## Status Changes ### `fluent_crm/subscriber_status_changed` This action fires whenever a subscriber's status changes, providing both old and new status values. This is the general status change hook — the dynamic hook below also fires alongside it. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$oldStatus` string - previous status - `$newStatus` string - new status ::: warning Older Pro versions pass only two arguments FluentCampaign Pro 3.1.10 and earlier fired this hook from the **Change Contact Status** automation action without `$newStatus`, so a callback declared with three *required* parameters threw an `ArgumentCountError` when that action ran. If you support those versions, give the third parameter a default and fall back to the model: ```php add_action('fluent_crm/subscriber_status_changed', function($subscriber, $oldStatus, $newStatus = null) { $newStatus = $newStatus ?: $subscriber->status; }, 10, 3); ``` ::: **Usage:** ```php add_action('fluent_crm/subscriber_status_changed', function($subscriber, $oldStatus, $newStatus) { // React to any status change }, 10, 3); ``` **Source:** `app/Models/Subscriber.php`, `app/Http/Controllers/SubscriberController.php`, `fluentcampaign-pro/app/Services/Funnel/Actions/ChangeContactStatusAction.php` --- ### `fluentcrm_subscriber_status_to_{$new_status}` This dynamic action hook fires when a subscriber's status has been changed to a specific new status. **Possible Hooks** One per status returned by [`fluent_crm/contact_statuses`](/hooks/filters/contacts#fluent-crm-contact-statuses): - `fluentcrm_subscriber_status_to_subscribed` - `fluentcrm_subscriber_status_to_pending` - `fluentcrm_subscriber_status_to_unsubscribed` - `fluentcrm_subscriber_status_to_transactional` - `fluentcrm_subscriber_status_to_bounced` - `fluentcrm_subscriber_status_to_complained` - `fluentcrm_subscriber_status_to_spammed` **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - already saved with the new status - `$oldStatus` string - old status of the contact ::: tip FluentCRM itself listens on several of these — `subscribed` resumes paused automations, while `unsubscribed`, `bounced`, `complained` and `spammed` run the unsubscribe cleanup. Your callback runs alongside those, not instead of them. ::: **Usage:** ```php add_action('fluentcrm_subscriber_status_to_subscribed', function($subscriber, $oldStatus) { // the subscriber got subscribed status. You can run your code here }, 10, 2); ``` **Source:** `app/Models/Subscriber.php`, `fluentcampaign-pro/app/Services/Funnel/Actions/ChangeContactStatusAction.php` --- ### `fluent_crm/subscriber_unsubscribed_from_web_ui` This action hook fires when a subscriber unsubscribes from the web UI. Please note that `fluentcrm_subscriber_status_to_unsubscribed` also fires before this action. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$postedData` array - post data of the unsubscribe form as key value pair **Usage:** ```php add_action('fluent_crm/subscriber_unsubscribed_from_web_ui', function($subscriber, $data) { // the contact unsubscribed from web UI. Do your stuff here }, 10, 2); ``` **Source:** `app/Hooks/Handlers/ExternalPages.php` --- ### `fluent_crm/subscriber_confirmed_via_double_optin` This action hook fires when a subscriber confirms via double optin by clicking the DOI link. Please note that `fluentcrm_subscriber_status_to_subscribed` also fires before this action. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) **Usage:** ```php add_action('fluent_crm/subscriber_confirmed_via_double_optin', function($subscriber) { // the contact confirmed the subscription via double optin }); ``` **Source:** `app/Hooks/Handlers/ExternalPages.php` --- ### `fluent_crm/subscriber_sms_status_changed` This action fires when a subscriber's SMS status is changed. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$oldStatus` string - previous SMS status - `$newStatus` string - new SMS status **Usage:** ```php add_action('fluent_crm/subscriber_sms_status_changed', function($subscriber, $oldStatus, $newStatus) { // SMS status changed }, 10, 3); ``` **Source:** `app/Http/Controllers/SubscriberController.php` --- ## Contact Type Changes ### `fluent_crm/subscriber_contact_type_to_{$new_type}` This action hook fires when a subscriber's `contact_type` has been changed to a new type. ::: warning Only fires from the bulk action This is dispatched from the **Change Contact Type** bulk action in the contacts list, once per contact whose type actually changed. Editing a single contact's type on the profile screen does not fire it. ::: **Possible Hooks** One per type returned by [`fluent_crm/contact_types`](/hooks/filters/contacts#fluent-crm-contact-types): - `fluent_crm/subscriber_contact_type_to_lead` - `fluent_crm/subscriber_contact_type_to_customer` **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - already saved with the new type - `$oldType` string - old type of the contact (eg: lead | customer) **Usage:** ```php add_action('fluent_crm/subscriber_contact_type_to_customer', function($subscriber, $oldType) { // the contact's type changed to customer. You can run your code here }, 10, 2); ``` **Source:** `app/Http/Controllers/SubscriberController.php` --- ## Unsubscribe from Email ### `fluent_crm/before_contact_unsubscribe_from_email` This action runs just after a contact unsubscribes by clicking the unsubscribe link in an email or from the email header. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$campaignEmail` [CampaignEmail Model](/database/models/campaign-email) or null - `$scope` string - `'from_header'` or `'web_ui'` **Usage:** ```php add_action('fluent_crm/before_contact_unsubscribe_from_email', function($subscriber, $campaignEmail, $scope) { // Do your stuff here }, 10, 3); ``` **Example:** Unsubscribe a user from specific lists instead of globally, depending on the sending lists: ```php add_action('fluent_crm/before_contact_unsubscribe_from_email', function($subscriber, $campaignEmail, $scope) { if(!$campaignEmail || !$campaignEmail->campaign) { return false; } $settings = $campaignEmail->campaign->settings; $sendingType = \FluentCrm\Framework\Support\Arr::get($settings, 'sending_filter'); if($sendingType != 'list_tag') { return false; } $sendingListIds = []; foreach ($settings['subscribers'] as $segment) { $sendingListIds[] = \FluentCrm\Framework\Support\Arr::get($segment, 'list', 0); } $sendingListIds = array_values(array_filter(array_unique($sendingListIds))); $sendingListIds = array_map('intval', $sendingListIds); if(empty($sendingListIds)) { return false; } $subscriber->detachLists($sendingListIds); wp_send_json_success([ 'message' => 'You are unsubscribed from the lists', 'redirect_url' => '' ], 200); }, 10, 3); ``` **Source:** `app/Hooks/Handlers/ExternalPages.php` --- ## Contact Notes ### `fluent_crm/note_added` This action fires when a note is added to a contact. **Parameters** - `$subscriberNote` [SubscriberNote Model](/database/models/subscriber-note) - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$note` Array - note data **Usage:** ```php add_action('fluent_crm/note_added', function($subscriberNote, $subscriber, $note) { // A note was added to the contact }, 10, 3); ``` **Source:** `app/Http/Controllers/SubscriberController.php` --- ### `fluent_crm/note_updated` This action fires when a contact note is updated. **Parameters** - `$subscriberNote` [SubscriberNote Model](/database/models/subscriber-note) - `$subscriber` [Subscriber Model](/database/models/subscriber) - `$note` Array - updated note data **Usage:** ```php add_action('fluent_crm/note_updated', function($subscriberNote, $subscriber, $note) { // A contact note was updated }, 10, 3); ``` **Source:** `app/Http/Controllers/SubscriberController.php` --- ### `fluent_crm/note_delete` This action fires after a contact note is deleted from the REST API, both for a single delete and once per note for the bulk delete. ::: warning The note row is already gone when this fires, and only its ID is passed. Capture what you need on `fluent_crm/note_added` or `fluent_crm/note_updated`. ::: **Parameters** - `$noteId` INT - Note ID - `$subscriber` [Subscriber Model](/database/models/subscriber) **Usage:** ```php add_action('fluent_crm/note_delete', function($noteId, $subscriber) { // A contact note was deleted }, 10, 2); ``` **Source:** `app/Http/Controllers/SubscriberController.php` --- ### `fluent_crm/note_deleted` The MCP counterpart of `fluent_crm/note_delete`, fired when a note is deleted through the MCP contact tools rather than the admin REST API. ::: warning Different name, different signature Note the past-tense name and that the second argument is a **contact ID**, not a model. Hook both `fluent_crm/note_delete` and `fluent_crm/note_deleted` if you need to cover every deletion path. ::: **Parameters** - `$deletedId` INT - Note ID - `$subscriberId` INT - Contact ID the note belonged to **Usage:** ```php add_action('fluent_crm/note_deleted', function($deletedId, $subscriberId) { // A contact note was deleted via MCP }, 10, 2); ``` **Source:** `app/Modules/MCP/Tools/ContactTools.php` --- ## Birthday ### `fluentcrm_contact_birthday` Fires when a contact's birthday occurs. Processed in batch during scheduled birthday checks. Used for triggering birthday automations. **Parameters** - `$subscriber` [Subscriber Model](/database/models/subscriber) **Usage:** ```php add_action('fluentcrm_contact_birthday', function($subscriber) { // Send birthday greeting, apply tags, etc. }); ``` **Source:** `fluentcampaign-pro/app/Hooks/Handlers/IntegrationHandler.php` --- ## Bulk Import ### `fluentcrm_contacts_imported_bulk` This action fires once at the end of every bulk import run with all the contacts the run inserted. The CSV importer, the WP-user importer, and the integration importers all funnel through `Subscriber::import()`, so they all fire it. ::: tip Fires even for silent imports Unlike [`fluent_crm/contact_created`](#fluent-crm-contact-created), this hook is **not** suppressed by the `FLUENTCRM_DISABLE_TAG_LIST_EVENTS` constant. When an admin imports silently, this hook and [`fluentcrm_contacts_updated_bulk`](#fluentcrm-contacts-updated-bulk) are the only signals that the rows arrived. ::: **Parameters** - `$insertedModels` Array - the newly inserted [Subscriber Models](/database/models/subscriber). An empty array when the run only matched existing contacts. **Usage:** ```php add_action('fluentcrm_contacts_imported_bulk', function($insertedModels) { foreach ($insertedModels as $subscriber) { // Sync each newly imported contact to your system } }); ``` **Source:** `app/Models/Subscriber.php` --- ### `fluentcrm_contacts_updated_bulk` This action fires immediately after `fluentcrm_contacts_imported_bulk`, with the already-existing contacts the import run matched by email. ::: warning Includes unchanged contacts Every matched existing contact is included, even when its incoming row changed nothing — unlike the per-row [`fluent_crm/contact_updated`](#fluent-crm-contact-updated), which only fires for rows with actual changes. The collection is empty when the import ran with "update existing contacts" disabled. ::: **Parameters** - `$updatedModels` Collection - [Subscriber Models](/database/models/subscriber) of the existing contacts matched by the import **Usage:** ```php add_action('fluentcrm_contacts_updated_bulk', function($updatedModels) { foreach ($updatedModels as $subscriber) { // React to re-imported contacts } }); ``` **Source:** `app/Models/Subscriber.php` --- ## Bulk Deletion ### `fluentcrm_before_subscribers_deleted` This action fires before contacts are deleted in bulk. **Parameters** - `$contactIds` Array - IDs of contacts about to be deleted **Usage:** ```php add_action('fluentcrm_before_subscribers_deleted', function($contactIds) { // Clean up related data before contacts are deleted }); ``` **Source:** `app/Services/Helper.php` --- ### `fluentcrm_after_subscribers_deleted` This action fires after contacts have been deleted in bulk. **Parameters** - `$contactIds` Array - IDs of contacts that were deleted **Usage:** ```php add_action('fluentcrm_after_subscribers_deleted', function($contactIds) { // Post-deletion cleanup }); ``` **Source:** `app/Services/Helper.php` --- ## Advanced Filter Providers ### `fluentcrm_contacts_filter_{$provider}` This dynamic action is the extension point behind the contacts **Advanced Filter** UI — and everything built on it: saved segments, campaign recipient selection, and the Pro automation conditions. For every filter group, the [ContactsQuery service](/helpers/contacts-query) fires one action per provider via `do_action_ref_array()`, handing the listener the query builder so it can add its `WHERE` constraints in place. The return value is ignored — the query object itself is the contract. **Possible Hooks** Core registers `subscriber`, `segment`, `custom_fields`, `activities`, and `event_tracking` (its listener is registered even while the experimental Event Tracking feature is off — only the Advanced Filter UI hides it). Integrations register their own providers: `woo`, `edd`, `learndash`, `lifterlms`, `tutorlms`, and `aff_wp` come from FluentCampaign Pro, and `fluent_cart` is registered by the Fluent Cart plugin. **Parameters** - `$query` Query Builder - the nested where-group for the current filter group. Add constraints to it in place; do not execute it. - `$filterItems` Array - the filter rows configured for this provider, each with `property`, `operator`, and `value` keys ::: danger Unhandled providers fail closed If no listener is registered for a provider name (`has_action()` returns false), ContactsQuery adds `whereRaw('1 = 0')` so the whole filter group matches **nothing**. That is deliberate: a provider that loses its handler (Pro deactivated, integration disabled) must not silently widen a campaign audience to "everyone". It also means your custom provider's listener must be registered on every request *before* the query runs — otherwise every segment or campaign using it quietly resolves to zero contacts. ::: ::: tip No `&` in your callback Although the hook is fired with `do_action_ref_array(..., [&$q, $items])`, the query builder is an object, so a plain `function ($query, $filterItems)` signature mutates it just fine — this is exactly how every core and Pro listener is declared. Do not add `&` to the parameter. ::: **Usage:** ```php add_action('fluentcrm_contacts_filter_my_plugin', function ($query, $filterItems) { foreach ($filterItems as $filterItem) { if ($filterItem['property'] == 'vip_level') { $query->where('fc_subscribers.total_points', '>=', (int) $filterItem['value']); } } }, 10, 2); ``` To surface your provider in the Advanced Filter UI, also register its fields through the `fluentcrm_advanced_filter_options` filter — the provider key there becomes the `{provider}` part of this hook name. **Source:** `app/Services/ContactsQuery.php`