Tutorials
Sending mass messages
Mass messages let creators broadcast a single DM to a targeted audience — for promo drops, PPV releases, or re-engagement of expired subscribers. This guide covers how to discover available lists, send a broadcast, and stay inside FrontRow’s rate limits.
Required scopes
The API key used for these calls must have write:chat and read:fan scopes. Generate the key in Settings → Developer with both boxes checked.
Examples below use curl for raw HTTP and Node.js / TypeScript for the helper. Translate to any language by porting the request shape.
01
Discover available lists
Every creator has two kinds of audience lists. Smart lists are maintained automatically based on fan behavior. Custom lists are groups the creator builds by hand.
Smart lists
| 1 | curl https://frontrow.center/api/v1/chats/lists/smart \ |
| 2 | -H "Authorization: Bearer $FRONTROW_API_KEY" |
Common smart list IDs:
all_fansAnyone who has ever followed or subscribed.
subscribersActive paid subscribers right now.
expired_subscribersSubscribers whose plan lapsed in the last 90 days.
top_spendersThe top 10% of fans ranked by lifetime spend.
recent_tippersFans who tipped in the last 30 days.
Smart list IDs are always lowercase. Passing Subscribers will return a 400.
Custom lists
| 1 | curl "https://frontrow.center/api/v1/chats/lists/custom?page=1&size=20" \ |
| 2 | -H "Authorization: Bearer $FRONTROW_API_KEY" |
Each custom list has a UUID, a name, and a member count. Preview the recipients before broadcasting:
| 1 | curl "https://frontrow.center/api/v1/chats/lists/custom/${LIST_ID}/members?page=1&size=10" \ |
| 2 | -H "Authorization: Bearer $FRONTROW_API_KEY" |
02
Send the broadcast
Mass sends go to POST /chats/mass-messages. A request must include either text or media (or both) and at least one entry in includeLists.
| 1 | curl https://frontrow.center/api/v1/chats/mass-messages \ |
| 2 | -H "Authorization: Bearer $FRONTROW_API_KEY" \ |
| 3 | -H "Content-Type: application/json" \ |
| 4 | -d '{ |
| 5 | "text": "New PPV drop tonight at 8pm — first 100 fans get it free.", |
| 6 | "includeLists": [ |
| 7 | { "type": "smart", "id": "subscribers" } |
| 8 | ], |
| 9 | "excludeLists": [ |
| 10 | { "type": "smart", "id": "recent_tippers" } |
| 11 | ] |
| 12 | }' |
The response includes the broadcast ID, total recipient count after exclusions, and a queued-at timestamp:
| 1 | { |
| 2 | "id": "mm_9a3c1b04", |
| 3 | "recipientCount": 847, |
| 4 | "queuedAt": "2026-04-28T20:00:00Z" |
| 5 | } |
Pay-to-view broadcasts
For PPV, attach media uploaded via the media endpoints and set a price (in cents, minimum 200 = $2.00).
| 1 | { |
| 2 | "text": "Behind-the-scenes from yesterday's shoot.", |
| 3 | "media": ["med_a1b2c3", "med_d4e5f6"], |
| 4 | "priceCents": 999, |
| 5 | "includeLists": [{ "type": "smart", "id": "top_spenders" }] |
| 6 | } |
03
TypeScript helper
A reusable service with retry-on-rate-limit and basic validation:
| 1 | type ListRef = { type: "smart" | "custom"; id: string }; |
| 2 | |
| 3 | interface MassMessage { |
| 4 | text?: string; |
| 5 | media?: string[]; |
| 6 | priceCents?: number; |
| 7 | includeLists: ListRef[]; |
| 8 | excludeLists?: ListRef[]; |
| 9 | } |
| 10 | |
| 11 | const API_BASE = "https://frontrow.center/api/v1"; |
| 12 | |
| 13 | export async function sendMassMessage(payload: MassMessage) { |
| 14 | if (!payload.text && !payload.media?.length) { |
| 15 | throw new Error("Mass message needs text or media."); |
| 16 | } |
| 17 | if (!payload.includeLists.length) { |
| 18 | throw new Error("At least one include list is required."); |
| 19 | } |
| 20 | |
| 21 | for (let attempt = 0; attempt < 5; attempt++) { |
| 22 | const res = await fetch(`${API_BASE}/chats/mass-messages`, { |
| 23 | method: "POST", |
| 24 | headers: { |
| 25 | Authorization: `Bearer ${process.env.FRONTROW_API_KEY}`, |
| 26 | "Content-Type": "application/json", |
| 27 | }, |
| 28 | body: JSON.stringify(payload), |
| 29 | }); |
| 30 | |
| 31 | if (res.status === 429) { |
| 32 | const retryAfter = Number(res.headers.get("Retry-After") ?? 2); |
| 33 | await new Promise((r) => setTimeout(r, retryAfter * 1000)); |
| 34 | continue; |
| 35 | } |
| 36 | |
| 37 | if (!res.ok) throw new Error(`Send failed: ${res.status}`); |
| 38 | return res.json(); |
| 39 | } |
| 40 | |
| 41 | throw new Error("Rate limit exceeded after 5 retries."); |
| 42 | } |
Rate limits & best practices
- Test with previews. Use the custom-list members endpoint to confirm the audience before broadcasting. Once queued, a mass message cannot be recalled.
- Respect 429s. Mass-message endpoints are limited to 10 broadcasts per hour per creator. Use exponential backoff and honor the
Retry-Afterheader. - Combine includes and excludes. A common pattern is
include: subscribers,exclude: recent_tippersto avoid messaging fans who just paid. - Track sends. Persist the returned
idand recipient count so you can join broadcast metadata against subsequent message-engagement webhooks. - Keep it human. Frequent identical broadcasts hurt reply rates and can trigger spam classifiers. Personalize at least the opener using fan handles from the list members endpoint.