Promote API
Promote (campaign outreach) lets you create campaigns, preview them, and send across channels with per-message pricing. Email sends are per-recipient via POST /v1/campaigns/{campaign_id}/send (there is no generic outreach/dispatch route). Routes are served under /v1/campaigns and backed by module-BDE and module-J.
Authentication
Authorization: Bearer ai92_live_<key> # production
Authorization: Bearer ai92_test_<key> # sandboxMutating requests support Idempotency-Key and are brand-gated.
Per-channel send pricing
The cost of POST /v1/campaigns/{id}/send is cost_per_message × messages_sent:
| Channel | Cost / message |
|---|---|
email | 1 credit |
whatsapp | 2 credits |
social_media | 2 credits |
sms_na (North America SMS) | 3 credits |
sms_intl (international SMS) | 6 credits |
Any other channel value is rejected with 422.
Credit costs at a glance
| Endpoint | Cost |
|---|---|
POST /v1/campaigns | Free |
GET /v1/campaigns/{id} | Free |
GET /v1/campaigns/{id}/preview | Free |
POST /v1/campaigns/{id}/send | Per channel (see table above) |
POST /v1/campaigns/upload-list | 2 cr per 1,000 records (rounded up) |
POST /v1/campaigns
Cost: Free
Create a campaign. Creation is free; credits are charged on send.
Auth: Bearer
| Field | Type | Required | Description |
|---|---|---|---|
brand_id | string | Yes | Brand the campaign belongs to. |
name | string | Yes | Campaign name. Length 1–128. |
channels | string[] | No | Channels the campaign targets. Default []. |
Request
curl https://api.ai92.ai/v1/campaigns \
-H "Authorization: Bearer ai92_test_51a9c0de…" \
-H "Content-Type: application/json" \
-d '{"brand_id": "acme", "name": "Spring launch", "channels": ["email", "whatsapp"]}'Response 200 OK
{
"campaign_id": "camp_77a2",
"brand_id": "acme",
"name": "Spring launch",
"channels": ["email", "whatsapp"],
"status": "draft"
}GET /v1/campaigns/{campaign_id}
Cost: Free
Retrieve a campaign by id.
Auth: Bearer
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | path | string | Yes | The campaign id. |
Request
curl https://api.ai92.ai/v1/campaigns/camp_77a2 \
-H "Authorization: Bearer ai92_test_51a9c0de…"Response 200 OK
{
"campaign_id": "camp_77a2",
"brand_id": "acme",
"name": "Spring launch",
"channels": ["email", "whatsapp"],
"status": "draft"
}GET /v1/campaigns/{campaign_id}/preview
Cost: Free
Render a preview of the campaign before sending.
Auth: Bearer
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | path | string | Yes | The campaign id. |
Request
curl https://api.ai92.ai/v1/campaigns/camp_77a2/preview \
-H "Authorization: Bearer ai92_test_51a9c0de…"Response 200 OK
{
"campaign_id": "camp_77a2",
"subject": "Meet the Spring lineup",
"preview_html": "<html>…</html>",
"estimated_recipients": 4200
}POST /v1/campaigns/{campaign_id}/send
Cost: Per channel — cost_per_message × messages_sent (see pricing table)
Send a campaign message. This is the correct send endpoint — there is no POST /v1/outreach/dispatch route.
Email is per-recipient. For the email channel there is no bulk-list concept: each call sends to exactly one recipient, and you supply the message subject and body inline. Loop the call to reach multiple people. Sending without a recipient returns 422 USE_SINGLE_RECIPIENT. Broadcast (list-wide) delivery is only available on the social_media channel.
send_mode does not control broadcast; it controls the approval workflow — auto sends immediately, otherwise the message enters manual review (see PATCH /v1/outreach/send-mode).
Auth: Bearer
| Field | In | Type | Required | Description |
|---|---|---|---|---|
campaign_id | path | string | Yes | The campaign to send. |
brand_id | body | string | Yes | The brand the campaign belongs to. |
recipient | body | string | Yes (email) | The single recipient address. Required for email; omitting it returns 422 USE_SINGLE_RECIPIENT. |
subject | body | string | Yes (email) | Email subject line. |
body | body | string | Yes (email) | Email body content. |
channel | body | string | No | One of email, whatsapp, social_media, sms_na, sms_intl. Default email. |
send_mode | body | string | No | Approval workflow: auto (send now) or manual review. Not a broadcast switch. |
Request
curl https://api.ai92.ai/v1/campaigns/camp_77a2/send \
-H "Authorization: Bearer ai92_test_51a9c0de…" \
-H "Content-Type: application/json" \
-d '{"brand_id": "acme", "recipient": "[email protected]", "subject": "Your subject line", "body": "Email body content"}'Response 200 OK
{
"campaign_id": "camp_77a2",
"recipient": "[email protected]",
"channel": "email",
"status": "sent",
"credits_charged": 1,
"balance_after": 1499
}Errors
| Status | Code | When |
|---|---|---|
422 | USE_SINGLE_RECIPIENT | email send with no recipient (email is per-recipient; loop the call). |
402 | INSUFFICIENT_CREDITS | Cannot reserve the required credits. |
402 | RESERVATION_REJECTED | Reservation rejected (e.g. per-key spending limit). |
422 | (validation) | channel is not a supported channel. |
503 | BACKEND_DISPATCH_FAILED | Backend dispatch failed; credits fully refunded. |
No campaign analytics/queue endpoints.
GET /v1/campaigns/{id}/analytics,GET /v1/campaigns/{id}/stats, andGET /v1/outreach/dispatch-queuedo not exist (404). For outreach state and feature flags useGET /v1/outreach/data; for individual async jobs pollGET /v1/jobs/{job_id}.
POST /v1/campaigns/upload-list
Cost: 2 credits per 1,000 records (rounded up per 1K pack)
Upload a contact list. Email validation is bundled at no extra charge when validate_emails=true. Cost = ceil(record_count / 1000) * 2.
Auth: Bearer
| Field | Type | Required | Description |
|---|---|---|---|
list_name | string | Yes | List name. Length 1–128. |
record_count | integer | Yes | Number of records (>= 1). Drives the cost. |
source_format | string | No | csv (default), json, or crm. |
validate_emails | boolean | No | Bundled email validation. Default true. |
Request
curl https://api.ai92.ai/v1/campaigns/upload-list \
-H "Authorization: Bearer ai92_test_51a9c0de…" \
-H "Content-Type: application/json" \
-d '{"list_name": "Q3 leads", "record_count": 2500, "source_format": "csv", "validate_emails": true}'Response 200 OK
{
"list_id": "list_3a9c",
"list_name": "Q3 leads",
"record_count": 2500,
"validated": true,
"credits_consumed": 6
}Errors
| Status | Code | When |
|---|---|---|
402 | INSUFFICIENT_CREDITS | Balance below the computed cost. |