Work with FluentCRM 3.x contact data through the public PHP API and ORM models. Covers Subscriber, Lists, Tag, User, ContactsQuery, createOrUpdate, list/tag attach and detach, custom fields, WP user linking, status protection, and contact hooks. Use when a plugin must create or update a contact, map a WP user, read or create lists/tags, apply tags/lists, query contacts or segments, or handle statuses such as subscribed, pending, transactional, unsubscribed, bounced, complained, and spammed. T...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-contact-models --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Fluentcrm Contact Models?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/lonsdale201-fluentcrm-contact-models)More formats (shields.io, HTML) on the badges page.
---
name: fluentcrm-contact-models
description: Work with FluentCRM 3.x contact data through the public PHP API and ORM models. Covers Subscriber, Lists, Tag, User, ContactsQuery, createOrUpdate, list/tag attach and detach, custom fields, WP user linking, status protection, and contact hooks. Use when a plugin must create or update a contact, map a WP user, read or create lists/tags, apply tags/lists, query contacts or segments, or handle statuses such as subscribed, pending, transactional, unsubscribed, bounced, complained, and spammed. Triggers on FluentCrmApi('contacts'), Subscriber, Lists, Tag, User, ContactsQuery, attachLists, attachTags, updateStatus, fluent_crm/contact_.
metadata:
wp-skills-author: "Soczó Kristóf"
wp-skills-contact: "mailto:lonsdale201@hotmail.com"
wp-skills-plugin: "fluent-crm"
wp-skills-plugin-version-tested: "3.1.13"
wp-skills-wp-version-tested: "7.1"
wp-skills-php-min: "7.4"
wp-skills-last-updated: "2026-08-25"
---
# FluentCRM: contact, list, tag, and user models
Use this skill for companion plugins that need to write or query FluentCRM contacts. Prefer `FluentCrmApi()` wrappers for writes, and use the ORM models for reads, reports, migrations, and carefully scoped queries.
Verification note: this skill is based on FluentCRM core 3.1.13 source. The contact/list/tag/user APIs covered here are core APIs and do not require FluentCampaign Pro.
## When to use this skill
- Creating or updating contacts from a third-party plugin, webhook, form, order, LMS event, or user registration.
- Adding or removing FluentCRM lists/tags from a contact.
- Querying contacts by list, tag, status, SMS status, company, search, custom field, or advanced filter provider.
- Mapping a WordPress user to a FluentCRM contact.
- Reviewing code that touches `Subscriber::create()`, `Subscriber::updateOrCreate()`, `attachLists()`, `attachTags()`, `fluentcrm_subscriber_statuses()`, or `ContactsQuery`.
## API entry points
Guard companion plugin code and run after FluentCRM has loaded:
```php
if (!function_exists('FluentCrmApi')) {
return;
}
$contactApi = FluentCrmApi('contacts');
$listApi = FluentCrmApi('lists');
$tagApi = FluentCrmApi('tags');
```
`app/Api/config.php` registers these keys: `contacts`, `tags`, `lists`, `extender`, `companies`, and `event_tracker`.
Prefer:
```php
$contact = FluentCrmApi('contacts')->createOrUpdate([
'email' => sanitize_email($email),
'first_name' => sanitize_text_field($firstName),
'last_name' => sanitize_text_field($lastName),
'user_id' => (int) $userId,
'status' => 'subscribed',
'source' => 'my-plugin',
'lists' => [3],
'tags' => [12],
'custom_values' => [
'plan' => sanitize_text_field($plan),
],
], false, false);
```
Do not create contacts with raw `$wpdb` inserts. Direct `Subscriber::create()` skips several integration-level behaviors. `FluentCrmApi('contacts')->createOrUpdate()` delegates to `Subscriber::updateOrCreate()`, syncs lists/tags/custom fields, links a WP user by email when possible, and fires the contact lifecycle hooks.
## Contact lookup
Use the API wrapper for common lookup:
```php
$contact = FluentCrmApi('contacts')->getContact($idOrEmail);
$contact = FluentCrmApi('contacts')->getContactByUserRef($userIdOrEmail);
$contact = FluentCrmApi('contacts')->getCurrentContact();
```
`getContactByUserRef($userId)` first checks `user_id`, then falls back to the WP user's email and saves the `user_id` on the contact if found. `Subscriber::getWpUser()` performs the inverse lookup and also removes duplicate `user_id` links from other contacts.
## Status rules
Use `fluentcrm_subscriber_statuses()` for the current status list. In FluentCRM 3.1.13 the local source returns:
```php
[
'subscribed',
'pending',
'unsubscribed',
'transactional',
'bounced',
'complained',
'spammed',
]
```
`fluentcrm_subscriber_editable_statuses()` excludes `bounced`, `complained`, and `spammed`. `fluentcrm_strict_statues()` returns `unsubscribed`, `bounced`, `complained`, and `spammed`.
Important write behavior:
- Without `$forceUpdate`, an existing `subscribed` contact is not downgraded by incoming `status`.
- Existing `bounced`, `complained`, and `spammed` contacts keep their status unless forced.
- Incoming `unsubscribed` is always respected.
- Use `$contact->updateStatus($status)` for an explicit status change; it fires `fluent_crm/subscriber_status_changed` and the legacy `fluentcrm_subscriber_status_to_{status}` hook.
## Double opt-in boundary
`createOrUpdate()` stores the contact and synchronizes the supplied relations,
but it does **not** send a double opt-in email. The caller must deliberately use
the two-step core flow:
```php
$contact = FluentCrmApi('contacts')->createOrUpdate([
'email' => sanitize_email($email),
'status' => 'pending',
'lists' => $serverOwnedListIds,
'tags' => $serverOwnedTagIds,
], false, false);
if ($contact && $contact->status === 'pending') {
$contact->sendDoubleOptinEmail();
}
```
Do not pass `$forceUpdate = true` from a public form merely to move an
unsubscribed/bounced/complained/spammed contact. Use
`fluentcrm-custom-optin-forms` for the re-consent status matrix, public endpoint
security, list-specific DOI precedence, confirmation hooks, and abuse controls.
## Custom fields
Pass custom fields under `custom_values`:
```php
FluentCrmApi('contacts')->createOrUpdate([
'email' => $email,
'custom_values' => [
'customer_tier' => 'gold',
'renewal_date' => '2026-12-31',
],
], false, false);
```
The third `createOrUpdate()` argument maps to `syncCustomFieldValues($values, $deleteOtherValues)`. Keep it `false` for incremental updates. Passing `true` allows empty submitted values to delete existing custom field meta.
## Lists and tags
Create or update list/tag definitions through the API wrappers:
```php
$lists = FluentCrmApi('lists')->importBulk([
[
'title' => 'Customers',
'slug' => 'customers',
'description' => 'Imported from My Plugin',
],
]);
$tags = FluentCrmApi('tags')->importBulk([
[
'title' => 'VIP',
'slug' => 'vip',
],
]);
```
`importBulk()` sanitizes title/slug/description, upserts by slug, and fires both legacy and current hooks:
- Lists: `fluentcrm_list_created`, `fluent_crm/list_created`, `fluentcrm_list_updated`, `fluent_crm/list_updated`
- Tags: `fluentcrm_tag_created`, `fluent_crm/tag_created`, `fluentcrm_tag_updated`, `fluent_crm/tag_updated`
Apply or remove lists/tags on a saved contact:
```php
$contact->attachLists([3, 4]);
$contact->attachTags([12]);
$contact->detachLists([4]);
$contact->detachTags([12]);
```
In 3.1.13 `attachLists()` and `attachTags()` return early for unsaved subscribers, sanitize IDs, use per-row `INSERT IGNORE`, refresh the relation, and only fire added hooks for IDs that were actually new. `detachLists()` and `detachTags()` read fresh pivot state and only fire removed hooks for rows actually deleted. `attachCompanies()` / `detachCompanies()` follow the same pivot-table pattern for the experimental Companies module, but their current hooks are legacy helper functions only; use `fluentcrm-companies-model` for company-specific APIs and hooks.
Do not pass public request values directly as lists/tags. The sanitizer accepts
names/slugs and may create missing definitions; numeric IDs are converted but do
not prove that the visitor is allowed to select that list/tag. Resolve a
server-owned choice map, verify the definitions exist, then pass only those IDs.
Current attach/detach hooks:
- `fluent_crm/contact_added_to_lists`
- `fluent_crm/contact_added_to_tags`
- `fluent_crm/contact_removed_from_lists`
- `fluent_crm/contact_removed_from_tags`
The callback receives `($subscriber, $ids)` for current hooks. Legacy helper hooks still exist and pass the ID list first.
## Query contacts
Use `ContactsQuery` through the API for segment-like reads:
```php
$contacts = FluentCrmApi('contacts')->query([
'with' => ['tags', 'lists'],
'search' => 'john',
'tags' => [12],
'lists' => [3],
'statuses' => ['subscribed', 'transactional'],
'sms_statuses' => ['sms_subscribed'],
'custom_fields' => true,
'sort_by' => 'created_at',
'sort_type' => 'DESC',
'limit' => 100,
])->get();
```
`ContactsQuery` allowlists sort columns before `orderBy()`. Do not pass unsanitized request values directly to ORM `orderBy()` in custom controllers.
For advanced filters, pass `filter_type => 'advanced'` and `filters_groups_raw`; FluentCRM formats groups and dispatches `do_action_ref_array('fluentcrm_contacts_filter_' . $providerName, [&$q, $items])`. Your custom advanced-filter provider must mutate the query by reference.
## Model notes
- `Subscriber` table: `fc_subscribers`; primary contact model; appended `full_name` and `photo`.
- `Subscriber.company_id` is the primary company pointer when the Companies module is enabled. Many-to-many company membership still lives in `fc_subscriber_pivot` with `object_type = FluentCrm\App\Models\Company`.
- `Lists` table: `fc_lists`; relation `subscribers()`, helpers `totalCount()` and `countByStatus()`.
- `Tag` table: `fc_tags`; relation `subscribers()`, helpers `totalCount()` and `countByStatus()`.
- `User` model maps the WordPress `users` table with primary key `ID`, hides `user_pass` and `user_activation_key`, and appends a contact-aware `photo`.
## Hooks to preserve
When replacing direct writes, ensure these still fire where relevant:
- `fluent_crm/contact_created`
- `fluent_crm/contact_updated`
- `fluent_crm/contact_email_changed`
- `fluent_crm/subscriber_status_changed`
- `fluent_crm/contact_custom_data_updated`
## What this skill does not cover
- Automation trigger/action/benchmark registration. Use `fluentcrm-funnel-trigger`, `fluentcrm-funnel-action`, or `fluentcrm-funnel-benchmark`.
- Email sequence enrollment and funnel subscriber state. Use `fluentcrm-automation-sequence-models`.
- Smart codes and dynamic segments. Use `fluentcrm-smartcodes-segments`.
- Companies / account records. Use `fluentcrm-companies-model`.
- Event tracking. Use `fluentcrm-event-tracking`.
- Public subscription, re-consent, and double opt-in orchestration. Use `fluentcrm-custom-optin-forms`.
## References
- FluentCRM docs: Subscriber, Lists, Tag, User, and Fluent ORM pages.
- Local source: `app/Api/Classes/Contacts.php`, `app/Models/Subscriber.php`, `app/Services/ContactsQuery.php`, `app/Functions/helpers.php`.
- Official documentation: <https://developers.fluentcrm.com/database/models/subscriber>
- Official documentation: <https://developers.fluentcrm.com/database/models/lists>
- Official documentation: <https://developers.fluentcrm.com/database/models/tag>
- Official documentation: <https://developers.fluentcrm.com/database/models/user>
- Official documentation: <https://developers.fluentcrm.com/database/orm/>
- Verified source paths:
- `fluent-crm/app/Api/config.php`
- `fluent-crm/app/Api/Classes/Lists.php`
- `fluent-crm/app/Api/Classes/Tags.php`
- `fluent-crm/app/Models/Lists.php`
- `fluent-crm/app/Models/Tag.php`
- `fluent-crm/app/Models/User.php`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!