Campaigns
Schedule and run bulk template sends — full CRUD, audience selection, lifecycle, and analytics.
A campaign is a bulk template send: pick a template, define an audience (groups + sources), set
a variable mapping that fills the template's {{n}} placeholders from each contact's fields,
and dispatch. The Public Campaigns API mirrors the dashboard's lifecycle endpoints, with two
deliberate omissions:
- No individual contact IDs in the audience. API users put contacts into a group first, then add the group to the campaign. This removes a class of unknown-number / opt-out edge cases from the API surface.
- No test send or CSV upload. Those are dashboard-only QA affordances for humans.
| Scope | Endpoints |
|---|---|
campaigns:read | List, get, audience preview, field coverage, errors summary / drilldown |
campaigns:write | Create, set audience, render preview, refresh metrics |
campaigns:send | Send now, schedule, pause, resume, stop |
Base path: /api/v1/public/campaigns
State machine
┌────────┐ set_audience ┌────────────┐ schedule ┌─────────────┐
│ DRAFT │ ──────────────────▶ │ DRAFT │ ─────────▶ │ SCHEDULED │
└────┬───┘ │ (audience │ └─────┬───────┘
│ send_now │ attached) │ │
▼ └─────┬──────┘ │ (scheduled_at arrives)
┌──────────┐ │ send_now │
│ RUNNING │ ◀───────────────────────┘ ◀─────────────────────── │
└────┬─────┘
│ pause resume
├──────────────────▶ PAUSED ──────────▶ RUNNING
│ stop
├──────────────────▶ STOPPED (terminal)
│ all recipients done
├──────────────────▶ COMPLETED (terminal)
│ fatal error
└──────────────────▶ FAILED (terminal) Terminal statuses: COMPLETED, STOPPED, FAILED. Each transition emits a corresponding
campaign.* webhook event.
Campaign object
| Field | Type | Notes |
|---|---|---|
id | UUID | |
name | string | |
description | string | null | |
status | string | One of DRAFT, SCHEDULED, RUNNING, PAUSED, COMPLETED, STOPPED, FAILED. |
template_id | UUID | |
template_name | string | null | |
template_category | string | null | MARKETING, UTILITY, AUTHENTICATION. |
template_language | string | null | |
variable_mapping | object | null | Recipe for filling template variables. Loosely typed — see Variable mapping. |
scheduled_at | ISO-8601 | null | UTC. |
started_at | ISO-8601 | null | |
completed_at | ISO-8601 | null | |
total_recipients | integer | Materialised once the campaign starts. |
stats | object | { total, sent, delivered, read, failed, pending }. |
created_at | ISO-8601 | null | |
updated_at | ISO-8601 | null |
GET /campaigns/{id} additionally returns an audiences array (group + source rows).
Variable mapping
The variable_mapping describes how to fill each template variable from contact fields. Its
exact shape is documented in the internal Campaigns README — the Public API accepts it as a
loose object so the underlying mapping grammar can evolve without breaking external clients.
The campaign engine validates it on the way in; if it's wrong you get a 422. Build the
mapping in the dashboard once, then read it back via GET /campaigns/{id} and reuse it.
CRUD
GET /campaigns — List campaigns
Newest first. Not paginated today.
curl https://api.lynkist.io/api/v1/public/campaigns \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK — { "data": [campaign, …] }.
POST /campaigns — Create a campaign
curl -X POST https://api.lynkist.io/api/v1/public/campaigns \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Idempotency-Key: 7c4d2e1a-…" \
-H "Content-Type: application/json" \
-d '{
"name": "May launch — India",
"description": "Marketing blast for May product launch",
"template_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"batch_size": 50,
"variable_mapping": { /* see Variable mapping */ },
"audience": {
"groups": ["g-1234…"],
"sources": ["s-5678…"]
},
"scheduled_at": "2026-06-02T09:30:00Z"
}'| Field | Required | Notes |
|---|---|---|
name | Yes | 1-120 characters. |
description | No | ≤ 500 characters. |
template_id | Yes | Lynkist's template UUID or the Meta template id. |
batch_size | No (50) | 1-1000. How many recipients are queued per Celery batch. |
variable_mapping | No | Required for templates with variables. Otherwise omit. |
audience | No | Groups + sources only. Can be set later. |
scheduled_at | No | If present, campaign starts in SCHEDULED; otherwise DRAFT. |
201 Created — returns the campaign object.
Emits campaign.created (and campaign.scheduled if scheduled_at is set).
GET /campaigns/{id} — Get a campaign
Returns the campaign object plus the audiences array.
curl https://api.lynkist.io/api/v1/public/campaigns/cb-… \
-H "Authorization: Bearer $LYNKIST_API_KEY"Audience
PUT /campaigns/{id}/audience — Set audience
Replaces the audience. Allowed only while DRAFT or PAUSED.
curl -X PUT https://api.lynkist.io/api/v1/public/campaigns/cb-…/audience \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "groups": ["g-1234…"], "sources": ["s-5678…"] }'GET /campaigns/{id}/audience/preview — Preview resolved audience
Resolves the groups + sources into the actual recipient set without materialising recipient rows in the database. Useful for "how many will this hit?" before send.
curl https://api.lynkist.io/api/v1/public/campaigns/cb-…/audience/preview \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK
{
"total": 1247,
"sample": [
{ "id": "c-1…", "first_name": "Anurag", "phone": "+91…" },
{ "id": "c-2…", "first_name": "Lara", "phone": "+44…" }
]
}POST /campaigns/{id}/audience/field-coverage — Field coverage report
For each field name you supply, returns how many of the resolved audience have it populated. Use this to catch "30% of recipients have no email" before send.
curl -X POST https://api.lynkist.io/api/v1/public/campaigns/cb-…/audience/field-coverage \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "fields": ["first_name", "email", "custom_fields.order_id"] }'200 OK
{
"total_recipients": 1247,
"coverage": {
"first_name": { "present": 1247, "missing": 0 },
"email": { "present": 812, "missing": 435 },
"custom_fields.order_id": { "present": 118, "missing": 1129 }
}
}Preview render
POST /campaigns/{id}/preview — Render a template preview
Resolves the campaign's variable_mapping against one sample contact and returns the rendered
template. If sample_contact_id is omitted, the first contact in the audience is used.
curl -X POST https://api.lynkist.io/api/v1/public/campaigns/cb-…/preview \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sample_contact_id": "c-1…" }'200 OK
{
"campaign_id": "cb-…",
"sample_contact_id": "c-1…",
"resolved_components": [
{ "type": "BODY", "text": "Hi Anurag, your order ORD-1042 has shipped." }
]
}Lifecycle
POST /campaigns/{id}/send — Send now
Materialises the audience (if not already) and starts dispatch immediately. Transitions
DRAFT or SCHEDULED → RUNNING.
curl -X POST https://api.lynkist.io/api/v1/public/campaigns/cb-…/send \
-H "Authorization: Bearer $LYNKIST_API_KEY"Emits campaign.started.
POST /campaigns/{id}/schedule — Schedule
Moves to SCHEDULED with a UTC start time.
curl -X POST https://api.lynkist.io/api/v1/public/campaigns/cb-…/schedule \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "scheduled_at": "2026-06-02T09:30:00Z" }'Emits campaign.scheduled.
POST /campaigns/{id}/pause — Pause
Pauses a RUNNING or SCHEDULED campaign. In-flight messages drain; new ones do not enqueue.
Emits campaign.paused.
POST /campaigns/{id}/resume — Resume
Resumes a PAUSED campaign. Emits campaign.resumed.
POST /campaigns/{id}/stop — Stop
Permanently terminates a campaign. Cannot be resumed. Recipients not yet dispatched stay in
pending forever (they remain queryable via the errors endpoints). Emits campaign.stopped.
Analytics
POST /campaigns/{id}/refresh-metrics — Recompute stats
Force-recomputes stats from the recipient rows. Stats normally update in the background; use
this if you want a fresh snapshot synchronously (e.g. before showing a dashboard widget).
curl -X POST https://api.lynkist.io/api/v1/public/campaigns/cb-…/refresh-metrics \
-H "Authorization: Bearer $LYNKIST_API_KEY"GET /campaigns/{id}/errors — Errors summary
Buckets failed recipients by Meta error code. Use it to see the shape of your failures at a glance — "300 hit code 131056 (marketing cap), 12 hit 470 (no template)…"
curl https://api.lynkist.io/api/v1/public/campaigns/cb-…/errors \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK
{
"total_failed": 312,
"by_code": [
{
"code": "131056",
"title": "Marketing template rate cap reached",
"fix": "Wait for the rolling 24h window to reset, or move to a different template.",
"count": 300,
"sample_message": "Meta: this number has hit the marketing message cap…"
},
{
"code": "470",
"title": "Template not found / disabled",
"fix": "Verify the template is still approved and not in a disabled state.",
"count": 12,
"sample_message": "Template 'order_confirmation' in en_US is disabled."
}
]
}GET /campaigns/{id}/errors/{error_code} — Errors drill-down
Lists individual recipients that failed with a specific code. Capped at limit=200 per call.
curl "https://api.lynkist.io/api/v1/public/campaigns/cb-…/errors/131056?limit=50" \
-H "Authorization: Bearer $LYNKIST_API_KEY"200 OK
{
"error_code": "131056",
"recipients": [
{
"id": "r-…",
"phone_number": "+91…",
"contact_id": "c-…",
"status": "failed",
"message_id": "wamid.HBgM…",
"error": "Meta: marketing template rate cap reached"
}
]
}Errors
| Status | When |
|---|---|
400 | Bad transition (e.g. pause on a terminal campaign), bad audience input |
401 | Missing / invalid API key |
403 | Key lacks the required scope (campaigns:read / write / send) |
404 | Campaign not found |
409 | Concurrent transition lost the race (rare; retry after re-reading the state) |
422 | variable_mapping shape invalid |
Webhooks
| Event | When |
|---|---|
campaign.created | After POST /campaigns |
campaign.scheduled | After POST /campaigns/{id}/schedule |
campaign.started | At dispatch start (manual or scheduled) |
campaign.paused | After POST /campaigns/{id}/pause |
campaign.resumed | After POST /campaigns/{id}/resume |
campaign.stopped | After POST /campaigns/{id}/stop |
campaign.completed | All recipients processed |
campaign.failed | Fatal error during dispatch (template disabled mid-flight, WABA disconnected, etc.) |