SendAfrica logoSendAfricaDocs

Bulk Campaigns Guide

From CSV to delivered campaign — import contacts, schedule, monitor live stats, and handle failures with per-recipient tracking.


This end-to-end guide builds a campaign workflow: import a contact list, schedule the send, watch live stats, and build a retry list from failures.

#1. Import your audience

step1.sh
bash
# Create a list
curl -s -X POST https://api.sendafrica.online/v1/contact-lists/ \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "June Promo"}'

# Import contacts from CSV
curl -s -X POST https://api.sendafrica.online/v1/contact-lists/3/import \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -F "file=@customers.csv"

# → { "imported": 482, "skipped": 3, "errors": [...] }

The import response includes row-level errors — fix those rows and re-import only the failures rather than the whole file (duplicates return 409 duplicate_contact).

#2. Estimate cost & check balance

step2.py
python
analysis = client.sms.analyze(
    "Mambo! Enjoy 20% off all weekend. Karibu!"
)
print(analysis.parts)  # 1

balance = client.credits.balance()
audience = 482

if balance.balance < analysis.credits * audience:
    print("Top up first — see /docs/api/payments")

#3. Schedule the campaign

POST/v1/campaigns/JWT
step3.sh
bash
curl -s -X POST https://api.sendafrica.online/v1/campaigns/ \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: june-promo-v1" \
  -d '{
    "name": "June Promo Blast",
    "message": "Mambo! Enjoy 20% off all weekend. Karibu!",
    "contact_list_id": 3,
    "scheduled_at": "2026-06-20T09:00:00Z",
    "sender_id": "MyBrand"
  }'

Best send windows (TZ)

Local experience says 08:00–11:00 and 17:00–20:00 EAT perform best. Avoid Friday-afternoon prayer times and month-end when airtime wallets run low.

#4. Monitor live stats

step4.ts
typescript
async function pollCampaign(jwt: string, id: number) {
  while (true) {
    const res = await fetch(
      `https://api.sendafrica.online/v1/campaigns/${id}`,
      { headers: { Authorization: `Bearer ${jwt}` } },
    );
    const { data } = await res.json();

    console.log(
      data.status,
      `${data.sent}/${data.total_recipients} sent,`,
      `${data.delivered} delivered`,
    );

    if (data.status === "completed" || data.status === "failed") break;
    await new Promise((r) => setTimeout(r, 30_000)); // worker ticks every 30s
  }
}

#5. Build a retry list from failures

step5.sh
bash
curl -s "https://api.sendafrica.online/v1/campaigns/42/recipients?status=failed" \
  -H "Authorization: Bearer $JWT_TOKEN"

Inspect each failure's reason: invalid_phone_number rows should be cleaned from the source list permanently; carrier timeouts can be retried once after 30 minutes.

#Operational notes

  • The worker sends per-recipient (chunks of 500), not as one gateway bulk call — credit accounting stays exact per contact.
  • Only draft/scheduled campaigns can be cancelled; processing campaigns are locked by a Redis distributed lock.
  • Opted-out contacts (STOP replies) are excluded automatically at execution time.
  • For ≤100 recipients one-off blasts, POST /v1/sms/bulk is simpler than a campaign.