Subscribers & fans
Live vs cached reads, new-subscriber detection, tags and notes.
Two related resources. Subscribers are per-account subscription records. Fans are the panel-wide CRM view of people, with tags, notes and spend.
Subscribers
# Cached — zero platform requests, reliable total
curl "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/cached?limit=100&offset=0" \
-H "X-API-Key: $KEY"
# Live — hits the platform
curl "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers" -H "X-API-Key: $KEY"Default to cached. See Cached reads.
Newly seen subscribers
curl "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/new" -H "X-API-Key: $KEY"Every incoming subscription — new subs and renewals, because a renewal moves the
fan's subscribed_at forward — newest first, from cache at zero platform cost.
There is no sync cursor. Without since/until you get the newest 50 of the
entire cache. Accepts since, until (inclusive ISO bounds on subscribed_at),
limit (1–500, default 50), offset, and type (all|active|expired).
For a genuine "what is new since I last looked" cursor, use
GET /events?types=new_subscriber&since=… instead.
Statistics
curl "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/stats?granularity=day" \
-H "X-API-Key: $KEY"granularity is hour, day, week or month. Aggregated server-side, so this
is one cheap request rather than bucketing thousands of rows yourself.
Keeping it fresh
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/refresh" -H "X-API-Key: $KEY"
# 202 → poll .../subscribers/refresh/statusA delta sync, so it is cheap enough to schedule. See Async jobs.
Expiry events carry counts; the cache carries names
The expired_subscriber event has no fan identity — only previous_total,
new_total and a negative delta, sampled every 10th poll.
But you do not need to diff snapshots to recover the who:
GET /subscribers/cached?type=expired returns them directly with each row's
expired_at, because is_active is recomputed from expired_at on every read
rather than frozen at sync time.
Fans
Panel-wide, across every connected account:
curl "$BASE/api/crm/$CRM/fans?limit=100&offset=0" -H "X-API-Key: $KEY"Each fan carries identity, tags, notes and total_spend. That figure is
MAX(platform lifetime total, sum of captured tip/purchase events) — not the
chargeback-corrected signed sum. Check spend_known: when it is 0, spend is
unknown rather than zero. See Earnings & transactions.
Tags
# Add — of_user_id is REQUIRED in the body
curl -X POST "$BASE/api/crm/$CRM/fans/$FANID/tags" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"tag": "vip", "of_user_id": "'$OFUID'"}'
# Remove — of_user_id in the body or the query string
curl -X DELETE "$BASE/api/crm/$CRM/fans/$FANID/tags/vip?of_user_id=$OFUID" \
-H "X-API-Key: $KEY"Tags are how you segment, and they can be applied automatically — a tag_fan
automation on new_purchase with an amount condition builds a
VIP list with no code.
Tags are not a mass-DM audience filter. To message a tagged segment, read the
ids from GET /fans and pass them as audience.fan_ids — see
Messaging.
Notes
curl -X PUT "$BASE/api/crm/$CRM/fans/$FANID/note" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"note": "Prefers video content.", "of_user_id": "'$OFUID'"}'One free-text note per fan, replaced on write; max 2000 characters, and an empty string clears it.
A fan is identified by account + fan, so `of_user_id` is required
The fans table is unique on (crm_id, of_user_id, fan_of_user_id), so the
panel-wide path alone cannot identify a row. All three of these routes return 400
without of_user_id. Tags cap at 40 characters.
Refreshing one fan's profile
curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/fans/$FANID/refresh-profile" \
-H "X-API-Key: $KEY"Pulls that fan's current profile from the platform. Targeted, so it costs one platform request rather than a full re-walk — good for a "refresh" button in a UI.
Per-fan transactions
curl "$BASE/api/crm/$CRM/accounts/$OFUID/fans/$FANID/transactions/cached" \
-H "X-API-Key: $KEY"A common workflow
Refresh, then read
Kick off subscribers/refresh and transactions/refresh, then read from the
cached routes. Do not block on the refresh — the cache always answers.
Tag automatically
An automation on new_purchase with payload.amount >= 100 and a
tag_fan action keeps a vip segment current without any code.
Segment and act
Read GET /fans, filter by tag and spend, then send a targeted
mass DM — with dry_run: true first to check the reach.