Skip to content

Run a campaign

  1. Terminal window
    curl https://k-message.kerneltics.com/v1/campaigns \
    -H "Authorization: Bearer km_live_YOUR_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: eid-2026-campaign" \
    -d '{ "name": "Eid offer", "template": "eid_promo" }'

    It is created in draft. Nothing sends.

  2. Up to 1000 per call, in as many calls as you need.

    for (const batch of chunk(customers, 1000)) {
    await post(`/v1/campaigns/${campaign.id}/recipients`, {
    recipients: batch.map(c => ({
    to: c.phone,
    name: c.name,
    parameters: { name: c.firstName, code: 'EID20' },
    })),
    })
    }

    Each batch is written in one transaction. If one entry is rejected none of them are stored, so the campaign never holds a list you believe it does not have. The error names the offending index, because nobody can find the bad row in a thousand otherwise.

  3. Terminal window
    curl https://k-message.kerneltics.com/v1/campaigns/{id}/start \
    -H "Authorization: Bearer km_live_YOUR_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "send_mode": "spread" }'
    { "status": "processing", "queued": 480, "skipped": 0 }

    queued is what went out now, not the total. Under spread, the rest follow on later days as the allowance refreshes.

    Add "scheduled_at": "2026-08-25T09:00:00Z" to arm it for a future time instead of starting now.

  4. Subscribe to campaign.completed and the final counts arrive when it is done. Or poll:

    Terminal window
    curl https://k-message.kerneltics.com/v1/campaigns/{id} \
    -H "Authorization: Bearer km_live_YOUR_KEY"
ModeRecipients who do not fit today
spread (default)Carried into following days until everyone is reached
today_onlySkipped, and counted in skipped

spread is right for anything that stays relevant, such as a product announcement. today_only is right for something that expires, such as a one-day offer, where sending it tomorrow is worse than not sending it.

A campaign that stops moving is usually not broken:

{
"status": "processing",
"resumes_at": "2026-08-21T00:05:00Z",
"throttle_reason": "Daily WhatsApp messaging limit reached"
}

That is spread doing its job. It resumes on its own.

GET /v1/accounts shows the number’s messaging_limit. It is Meta’s per-business-portfolio cap on distinct new conversations per rolling 24 hours, and it rises as your sending history and quality rating improve.

Terminal window
curl -X POST https://k-message.kerneltics.com/v1/campaigns/{id}/pause \
-H "Authorization: Bearer km_live_YOUR_KEY"

Recipients already handed to the workers still go out; the rest stay pending. Starting it again picks up where it left off.

Recipients can be added while a campaign is paused, but not while it is running: adding mid-send races the workers draining the queue, so whether the new rows went out would depend on timing.