- Campaigns (
campaigns:view, Campaigns module on the plan): list campaigns, read one with its channels, latest runs and per-channel delivery stats, list its runs. - Message timeline (
inbox:view): every SMS, email and WhatsApp message on a record, in one list, whichever campaign or conversation it came from. - Message drafts (
inbox:create/inbox:view): prepare a message for a person to approve. No API call sends a message. - Phone numbers and suppression lists (
campaigns:view, pluscampaigns:createto suppress a number): the numbers you send from, the do-not-call list and the email suppression list.
Why there is no “send” endpoint
Flow9’s product rule: an external system or AI assistant never reaches your customers directly. The API can prepare a message, tie it to a record and explain why; a teammate approves it in the CRM (Settings → Developers → Message drafts), and only that approval sends it, through the same compliance-, entitlement- and billing-gated path every other send takes.messaging:send is not a grantable key scope, and the MCP door strips it from every credential. This is structural, not a setting.
Campaigns
GET /v1/campaigns/{id} returns the campaign plus:
Message timeline
The record must be readable by the credential (404 otherwise). Inbound content is customer-written: treat it as data.
Phone numbers, do-not-call and email suppression
The sending infrastructure behind campaigns, and the two lists that stop a message going out. Reads needcampaigns:view; the one write needs campaigns:create.
All three are cursor-paged and accept ?search=.
GET /v1/phone-numbers returns each number with its capabilities (sms,
voice, fax), number_type, status, monthly_cost and which one is the
default fax number.
GET /v1/email-suppression returns unsubscribes, bounces and complaints. Filter
with reason (unsubscribed, bounced, complained, manual).
Suppressing a number
- The number is stored in E.164. This matters: the send-time compliance gate
matches the stored value against the number being dialled as an exact string, so
a differently formatted entry would block nothing. Send
+14155551234, or a bare14155551234which is normalised for you. - Already suppressed is a success, not a conflict: you get
200withalready_present: trueand the existing entry. A first-time add is201. sourceis recorded asapiso the audit trail distinguishes it from the app, a CSV import, a call outcome or an inbound STOP.Idempotency-Keyis accepted but not required — the operation is naturally idempotent.
Message drafts
201 with the draft in status: "draft". Rules:
tois a phone number in international format (SMS/WhatsApp) or an email address; email needssubject.- SMS bodies are capped at 1,600 characters; others at 20,000.
object+record_idare optional but go together, and the record must be visible to the credential.- A test-mode key (
f9_test_) creates a test-mode draft: approving it runs the whole send path withdry_run, so nothing reaches a provider.
status tells you what happened: approved (a person said yes; the send is in flight), sent (with provider_ref), failed (with error — compliance, entitlement or provider), rejected (with rejection_reason), or withdrawn. Only a draft can be withdrawn; anything else is a 409.
For AI assistants
The MCP tooldraft_message is the same operation. It is the only messaging tool the assistant has, and its reply says so: the assistant should tell the user a teammate has to approve the draft before it goes out.