Overview
Marketing audiences contain Algerian mobile numbers, a mandatory language, and values for the personal-information fields defined when the audience is created. Marketing campaigns own their localized message bodies and do not use message templates. A campaign must be submitted and approved by the back-office before its snapshotted recipients are queued through the normal credit and SMS delivery pipeline.
Every endpoint accepts the authenticated dashboard session cookie or an API key with the matching permission. Permissions are deliberately split by resource and access level:
| Permission | Operations | Key environment |
|---|---|---|
marketing:audiences:read |
List audiences and members. | Test or production |
marketing:audiences:write |
Create, rename, and delete audiences; add and remove members. | Test or production |
marketing:campaigns:read |
List campaigns and retrieve delivery results. | Production only |
marketing:campaigns:write |
Validate, snapshot, and submit campaigns for back-office review. | Production only |
Pass the key as Authorization: Bearer $BAR9_API_KEY. Read permissions do not imply write access, and write permissions do not imply read access. All operations remain owner-scoped. Campaign submission requires approved production access; audience and member management remain available before production approval.
Create an audience
POST /v1/marketing/channels
Requires marketing:audiences:write when authenticated with an API key.
{
"name": "Ramadan customers",
"fields": [
{ "key": "first_name", "label": "First name" },
{ "key": "city", "label": "City" }
]
}
The audience name is required, is limited to 120 characters, and must be unique within the account. An audience can define at most 20 personal fields. Keys must be unique snake-case names of at most 64 characters; phone_number and language are reserved. Labels are required and limited to 80 characters.
The response is 201 Created and contains the audience ID, immutable field definitions, subscriber and message counts, and timestamps. GET /v1/marketing/channels?page=1&per_page=25 lists audiences. PATCH /v1/marketing/channels/:channel_id accepts { "name": "New name" }; field definitions cannot be changed after creation. DELETE /v1/marketing/channels/:channel_id returns 204 No Content, but an audience already snapshotted by a campaign cannot be deleted.
Listing audiences requires marketing:audiences:read; renaming and deleting require marketing:audiences:write.
Add audience members
POST /v1/marketing/channels/:channel_id/numbers
{
"phone_number": "0555123456",
"language": "fr",
"fields": {
"first_name": "Amine",
"city": "Oran"
},
"consent": {
"source": "web_form",
"collected_at": 1786485600,
"proof_reference": "crm:contact:1234"
}
}
The phone number must be an Algerian mobile number and is stored in E.164 form. Language is mandatory and canonicalized (fr, ar, or en-US, for example). fields must contain every audience field exactly once: values are required, trimmed, limited to 500 characters, and cannot contain template delimiters. A phone number can appear only once in an audience. consent is mandatory: source identifies how the written opt-in was obtained, collected_at is its Unix timestamp, and proof_reference points to the durable evidence in your records. A globally opted-out number cannot be added again.
The response is 201 Created. List members with GET /v1/marketing/channels/:channel_id/numbers?page=1&per_page=25; each item includes its consent evidence and current validity. GET /v1/marketing/channels/:channel_id/numbers/:number_id/consent downloads the owner-scoped evidence record. DELETE /v1/marketing/channels/:channel_id/numbers/:number_id/consent revokes local consent; removing the member uses DELETE /v1/marketing/channels/:channel_id/numbers/:number_id.
Signed provider inbound webhooks at POST /v1/webhooks/sms/:provider/inbound record a body equal to STOP as a global opt-out. Global suppression is independent of accounts and audiences: once recorded, the normalized number is excluded from every customer’s future marketing campaign, even if an audience or account is later deleted.
Adding and removing members require marketing:audiences:write; listing them requires marketing:audiences:read.
Create and submit a localized campaign
POST /v1/marketing/campaigns
Requires an approved account and a production API key with marketing:campaigns:write.
{
"name": "Ramadan offer",
"audience_id": "mkt_123",
"variants": [
{ "language": "fr", "body": "Bonjour {{first_name}}, notre offre est disponible à {{city}}." },
{ "language": "ar", "body": "مرحباً {{first_name}}، عرضنا متاح في {{city}}." }
]
}
Provide 1 to 10 unique language variants, each with a non-empty body of at most 2,000 characters. Placeholders must use {{field_name}}, must reference audience field keys, and every localized variant must use the same placeholder set. Creation requires a non-empty audience of at most 10,000 members and a variant for every member language.
Validation and creation are atomic: any error returns no campaign record. Before saving, Bar9 renders every snapshotted message and calculates its current cost with the same carrier tariff and SMS-segment rules used at dispatch. If the available balance is lower than that exact estimate, the request returns 402 with the required and available credit amounts. Credits are not reserved while approval is pending, so the atomic dispatch check still applies if the balance changes later.
Call POST /v1/marketing/campaigns/estimate with the same body before confirmation. It performs the complete validation and pricing pass without creating a campaign, snapshot, message, or credit entry. The response contains cost_credits, balance_credits, total_recipients, billable_recipients, excluded_recipients, and affordable. Because audiences, consent, tariffs, and balances can change, POST /v1/marketing/campaigns repeats this exact preparation when the campaign is submitted.
Success returns 201 Created and immediately creates an immutable recipient/content snapshot with pending status—there is no draft state. Bar9 evaluates every localized variant against the administrator-configured prohibited-content rules and shows any automatic matches in the unified moderation desk; the campaign still cannot dispatch until that reviewer explicitly approves it. An approval or structured rejection is emailed to the submitting account, with delivery outcome retained for operational follow-up. Members without current consent or on the global STOP list are retained in the campaign report as exclusions and are omitted from pricing and delivery. Consent is checked again at dispatch, so revocation after approval still prevents sending. The possible campaign status values are pending, rejected, approved, processing, completed, failed, and cancelled.
Approved campaigns dispatch only during the daily marketing window configured by the back-office in the Africa/Algiers timezone. If approval or a worker retry occurs outside that window, Bar9 keeps the campaign approved and automatically defers its dispatch job until the next opening. This restriction applies only to marketing campaigns; transactional messages and OTP traffic continue through their normal queues.
GET /v1/marketing/campaigns?page=1&per_page=25 lists campaign state, variants, counts, review outcomes, and aggregate delivery statistics. GET /v1/marketing/campaigns/:campaign_id includes submitted_at, reviewed_at, per-language statistics, localized previews, and average_delivery_latency_ms. It also returns exclusions with each refused number, stable reason (consent_missing_or_revoked or globally_opted_out), and timestamp. statistics.excluded reports consent/STOP exclusions separately from delivery failures, while statistics.moderation_blocked reports otherwise billable recipients stopped by a rejected campaign. Both fields are also present in every per-language breakdown.
Both campaign read endpoints require a production API key with marketing:campaigns:read when a session is not used.
Campaigns cannot be edited; submit a new campaign after a rejection. The former POST /v1/marketing/channels/:channel_id/messages endpoint returns 403 because immediate sending would bypass approval.
Pagination
Audience, member, and campaign lists accept page and per_page. Defaults are 1 and 25; per_page is capped at 100. List responses use the standard { "ok": true, "data": [...], "pagination": { "limit", "offset", "total", "has_more", "next_offset" } } envelope.
Common errors
Errors use { "ok": false, "error": { "code": "validation_error", "message": "..." } }.
| Status | Typical cause |
|---|---|
400 |
Invalid name, fields, language, values, placeholders, variants, or audience size. |
401 |
Missing or invalid dashboard session or API key. |
403 |
Missing permission, wrong key environment, production access not approved, or disabled immediate send. |
404 |
The audience, member, or campaign does not belong to the authenticated user. |
409 |
Duplicate audience/member, empty audience, protected audience deletion, or concurrent audience change. |
402 |
The current balance cannot cover the fully rendered campaign snapshot. |
405 |
The path exists but does not support the HTTP method; error.code is method_not_allowed. |