Resources
Changelog
Every change to the Fansquare API, newest first, including additive changes that need no new version.
Fansquare API versions are named after minerals, in alphabetical order.
The Fansquare-Version header accepts the alias without accents (Amethyste) or the ISO date.
A version remains supported for at least 18 months after the release of the next one.
Backward-compatible additions (no new mineral)
Additive changes: available on every version, they do not create a new mineral (older clients ignore what they do not know).
2026-10-13: Fixes
Webhook endpoints follow the creator scope of the key. A key restricted to some creators only sees, and only acts on, the endpoints that cover nothing but its own creators: an endpoint of the whole agency or one that lists another creator reads as
404 unknown_webhook_endpoint. On create and update, an emptyallowed_creator_idsmeans the creators of the key, and a creator outside its scope returns404 unknown_creator. Keys over the whole agency are unchanged.Request bodies refused before the route return a 4xx, not a 500. Malformed JSON returns
400 validation_failed, a body over 256 KB413 payload_too_large, a body that is not JSON415 unsupported_media_type. An empty body sent withContent-Type: application/jsonis accepted on endpoints that take no body (DELETE, ping, rotate, replay).Schema errors list a
pathon every item oferrors(the existing fields stay).Analytics windows west of UTC. A day in
start/endis read intimezone, as documented: withtimezone=America/New_York,2026-07-01used to start on June 30 local time. A day that does not exist (2026-02-30) is now refused with400 validation_failedinstead of rolling over to March 2.Expired keys are refused from their exact
expires_at, not up to 5 minutes later.
2026-10-13: Fansquare names
Offmy is now Fansquare. The API takes the new name everywhere it shows, and keeps accepting and sending the former names: an integration built before the rename keeps working unchanged.
Host:
https://api.fansquare.so.https://developers.offmy.coserves the same API.Version header:
Fansquare-Version.Offmy-Versionstays accepted; when both are sent,Fansquare-Versionwins.API keys: new keys start with
fsq_live_. Keys starting withoffmy_live_stay valid.Webhook deliveries carry
Fansquare-Signature,Fansquare-Signature-Timestamp,Fansquare-Event-Type,Fansquare-Event-IdandFansquare-Delivery-Id, and still carry the same values under theirOffmy-names (Offmy-SignatureandFansquare-Signaturehold the same signature).Webhook signing secrets: new secrets start with
fsq_whsec_. Existingoffmy_whsec_secrets keep signing.documentation_urlin version errors points tohttps://fansquare.so/api/versioning.Unchanged until a future version: the
typeURI of errors (https://developers.offmy.co/errors/<slug>) and theUser-Agentof webhook deliveries (Offmy-Webhooks/1.0).
2026-10-08: Fan tags and audience filters
The tags set on the fan card in Offmy ("VIP", "timewaster"…) are now
exposed, and the fan list gains the filters of the CRM Audience. No new
scope: read:fans is enough.
GET /v1/accounts/{account_id}/fansandGET …/fans/{fan_id}: new fieldtags, a list of{ id, name }(empty when the fan has no tag).GET /v1/tags: catalog of the agency's tags (id,name,color), in CRM order. Tags are agency-level: the creator scope of the key does not apply.New optional filters on
GET /v1/accounts/{account_id}/fans, combined with AND (OR within a list):tag_ids: carries at least one of these tags;exclude_tag_ids: carries none of these tags (fans without tags included);has_tags(true/false): has at least one tag / has none;statuses:active_subscriber,former_subscriber,interested;min_spend_cents: total à la carte spend on this account, in euro cents (5000= 50 €);last_purchase_after/last_purchase_before: date of the last à la carte purchase (fans who never purchased do not matchlast_purchase_before);is_online(true/false): current MYM presence.
"À la carte" = tips, private media, pushes. Subscriptions and refunded purchases are excluded.
Tag filters are checked:
tag_idswithhas_tags=false, or the same id in bothtag_idsandexclude_tag_ids, return 400validation_failed(the request could never return anything). An id that is not a tag of the agency returns 422unknown_tag, with the offending ids intag_ids.
2026-09-01: Sent messages (GET /v1/sent-messages)
New flat resource (agency resources, Offmy ids) and new scope read:messages:
the send activity log of the employees, without the message content.
GET /v1/sent-messages(?user_ids,creator_ids,sources,start,end,cursor,limit): itemsid,sent_at,source(messaging/script; Offmy mass sends and native MYM pushes are not listed),sent_by_user_id(id fromGET /v1/employees, null = unattributed send),creator_id,fan_id(MYM id),fan_username,is_private_media,price_eur(cents),sale_status(free/pending/purchased/refused),message_id(MYM id of the message, null when not reconciled yet) andmedia(MYM ids of the attached media, with their type).The message content is never included:
message_idandmediaare the pointers to fetch it directly from MYM with your own access.Sorted from the most recent to the oldest, cursor pagination, rate limit 20/min.
Served from a statistics projection: freshness of about 15 minutes, history of about 24 months per connected account.
2026-07-22: Insight note concurrency token readable in the body
The concurrency token (formerly "ETag") only lived in the ETag header of the
response. A header is invisible in most no-code tools (and it was missing from the
generated docs, which only described it in prose). It is now emitted and accepted
on both carriers, with the same value and the same semantics:
GET .../insight→{ note, etag }in addition to theETagheader. In the body the token is bare, without quotes (the quotes are HTTP syntax).PATCH .../insightaccepts the token in theetagbody field or in theIf-Matchheader. Both are lenient (quoted, bare,W/prefix). 400validation_failedif both are sent and differ, 428 if neither is, 412 unchanged.The PATCH response also returns
{ note, etag }+ theETagheader.
Additive: integrations that already send If-Match do not change by a single
byte. The generated docs now expose the ETag header and the If-Match
parameter in OpenAPI (they were missing, hence misleading request examples).
2026-07-22: Every set filter is now plural
Restriction (not an addition). Rule that now applies across the whole API:
a query parameter that selects a subset is plural and takes a CSV list,
even for a single value (?creator_ids=abc, ?order_types=tip). This
applies to identifiers and to enumerations alike. There is no singular twin
anymore.
| Endpoint | Before | After |
|---|---|---|
GET /v1/analytics/* (all 4) | creator_id + creator_ids | creator_ids |
GET /v1/analytics/revenue/timeseries | user_id | user_ids |
GET /v1/orders | creator_id | creator_ids |
GET /v1/orders | fan_id | fan_ids |
GET /v1/orders | order_type | order_types |
GET /v1/accounts/{id}/fans | assigned_user_id | assigned_user_ids |
Left untouched, on purpose: URL segments (/v1/creators/{creator_id})
identify a single resource, write bodies (PUT …/assignment { user_id })
write a single value, and response fields keep their natural cardinality
(creator_id in an order item designates a creator, it is not a filter).
Why normalize rather than keep both forms: creator_ids is a strict superset
of creator_id, so the singular brought nothing but a second spelling. Their
coexistence on the analytics was even a trap, the two being merged as a
union: ?creator_id=A&creator_ids=B,C returned the scope {A, B, C} with no
error and no precedence.
Querystrings are now .strict(): an unknown parameter is a 400. This is what makes this renaming safe. Without it, ?creator_id=abc would have been
silently ignored and the caller would have received the figures of all
creators while believing they were filtered. A wrong result is worse than an
error. An empty CSV (?creator_ids=) is a 400 for the same reason, rather than
meaning "all".
Widened along the way: user_ids accepts several employees and counts the
revenue attributed to any of them. No risk of double counting:
app.order_attributions.order_id is UNIQUE.
2026-07-22: Analytics window caps
Restriction (not an addition): the analytics now cap the width of the
window per bucket size. The cost of an aggregate is proportional to the number
of orders in the window (the granularity only changes the GROUP BY, not the
scan), so the cap applies to the window.
granularity | Max window |
|---|---|
hour | 7 days |
day | 31 days |
week / month | 365 days |
| (endpoints without granularity) | 365 days |
GET /v1/analytics/creators | 90 days |
/v1/analytics/creators is capped lower than the others: its subscriber
counters are COUNT(DISTINCT), which must sort every row of the window instead
of summing them as the scan goes. Measured on the largest agency: 31 d ≈ 185 ms,
90 d ≈ 323 ms, 365 d ≈ 8.2 s (the sort spills to disk). The three other
endpoints only compute SUM and stay under a second over a full year.
Before: 730 days everywhere, hour ≤ 31 days. A window that is too wide returns
400 validation_failed; fetching more history takes several requests.
The automatic default of granularity changes from "≤48h" to "≤2 days" for
hour (only windows that cross a clock change switch bucket).
Shipped without a new mineral: the public API is in preview and has two active keys. A restriction of this kind on a widely installed API would go through a new mineral.
2026-07-21: Analytics
Ready-made revenue aggregates (no more downloading the whole /orders ledger
to build a chart). Window: start/end are inclusive days interpreted in
timezone (canonical IANA name Area/Location, UTC by default); amounts in EUR
cents; affiliate revenue is grouped under affiliation.
GET /v1/analytics/revenue: total +by_typeover the window.GET /v1/analytics/revenue/timeseries: gap-filled buckets for charts,granularity(hour|day|week|month, automatic default),breakdown=creator,user_idfilter (revenue attributed to an employee).GET /v1/analytics/creators: revenue by type + subscriptions (new,renewal,paying_fans) per creator.GET /v1/analytics/employees: attributed revenue per employee +unattributed_amount/former_employees_amount.Common filters:
creator_id,creator_ids(CSV, max 50),timezone.New scope:
read:analytics.
2026-07-17: Fan insight note
GET /v1/accounts/{account_id}/fans/{fan_id}/insight(scoperead:insights) →{ note }+ETagheader.PATCH /v1/accounts/{account_id}/fans/{fan_id}/insight(scopewrite:insights): body{ note? },If-Matchheader required (428 otherwise, 412 if the note changed since it was read). Optimistic concurrency + edit history.New scopes:
read:insights,write:insights.
Améthyste (2026-07-06)
Initial version.
Agency resources (Offmy ids):
GET /v1/creators,GET /v1/creators/{creator_id},GET /v1/employees,GET /v1/orders,GET /v1/orders/{order_id}.Per-fan relations (MYM ids):
GET /v1/accounts/{account_id}/fans,GET /v1/accounts/{account_id}/fans/{fan_id},PUT|DELETE /v1/accounts/{account_id}/fans/{fan_id}/assignment,PUT|DELETE /v1/accounts/{account_id}/fans/{fan_id}/ai-lock.Meta:
GET /v1/_health,GET /status,GET /changelog,GET /v1/me,GET /v1/openapi.json,/docs.