Once a campaign launches, you can watch its progress in real time and inspect the final delivery breakdown when it completes. The Briq dashboard shows all of this on the campaign view — counters, per-batch status, recipient list, delivery rate. The developer API exposes the same data through three read-only endpoints.
Status translation. Every run-status field in the responses below translates the engine’s internal claimed sentinel to running. As an API consumer you will never see claimed — treat running as the in-flight state.
1. Live status — for polling loops
Dashboard. The campaign view updates automatically as batches complete. You see the current batch / total batches and live counters for sent, failed, skipped, and delivered.
Developer API.
GET https://karibu.briq.tz/v1/campaign/{campaign_id}/status
- Returns the latest run’s execution status plus batch progress counters.
- Mirrors the user-facing
/campaigns/{id}/status endpoint shape — the same fields whether you call the developer or the user surface.
- This is the endpoint to poll while a campaign is active.
Success response (run exists)
{
"success": true,
"data": {
"campaign_id": "8d2e1f6a-...",
"campaign_run_id": "run_789",
"status": "running",
"current_batch": 1,
"total_batches": 2,
"recipient_count": 1500,
"sent": 1000,
"failed": 5,
"skipped": 0,
"delivered": 800,
"updated_at": "2026-05-12T09:14:02.218Z"
},
"errors": null,
"request_id": "req_01J..."
}
Response when no run yet
{
"success": true,
"data": {
"campaign_id": "8d2e1f6a-...",
"status": "NO_RUN"
},
"errors": null,
"request_id": "req_01J..."
}
NO_RUN is a sentinel, not a run status. When the campaign has zero runs, the endpoint returns the literal string "NO_RUN" in status. This value is not part of the normal run-status enum (scheduled, running, completed, failed, cancelled). Check for status === "NO_RUN" before treating status as a run state.
Field reference
| Field | Type | Notes |
|---|
campaign_id | string (uuid) | The campaign you queried. |
campaign_run_id | string | null | Latest run’s id. Absent / null when status === "NO_RUN". |
status | string | One of scheduled, running, completed, failed, cancelled, or NO_RUN. |
current_batch | int | Count of completed batches, not the batch currently in flight. |
total_batches | int | Total batch count for the latest run. |
recipient_count | int | Recipients on this run. |
sent | int | Per-recipient submit successes recorded so far. |
failed | int | Per-recipient submit failures recorded so far. |
skipped | int | Recipients skipped (e.g. invalid number, dedupe). |
delivered | int | Delivery-report confirmations recorded so far (lags sent). |
updated_at | string (ISO 8601) | Last time the counters or status were touched. |
Errors
| HTTP | When |
|---|
| 401 | Missing or invalid X-API-Key. |
| 403 | API key is valid but does not own this campaign’s workspace. |
| 404 | campaign_id does not exist. |
| 500 | Server error — safe to retry with backoff. |
Code samples
curl -X GET "https://karibu.briq.tz/v1/campaign/8d2e1f6a-.../status" \
-H "X-API-Key: YOUR_API_KEY"
Polling cadence
A reasonable polling rhythm.
- 5–15 seconds is fine while
status is scheduled or running.
- Stop polling as soon as
status flips to completed, failed, or cancelled — counters are terminal at that point.
- Then call
/analytics once to grab the final rollup. Do not keep polling /status after the terminal transition.
2. Aggregate analytics
Dashboard. The campaign’s “Analytics” tab shows total recipients, sent / delivered / failed / skipped counts, plus delivery and failure rates as percentages.
Developer API.
GET https://karibu.briq.tz/v1/campaign/{campaign_id}/analytics
- Returns campaign-wide aggregate analytics across all runs (delivery rate, failure rate, counts, timing).
- When no analytics row has been written yet (e.g. a campaign created but never run, or polled in the first seconds after launch), the endpoint returns a zeroed-out payload with
null rate fields. HTTP status is still 200 — check delivery_rate !== null to know whether percentages are renderable.
Success response (populated)
{
"success": true,
"data": {
"campaign_id": "8d2e1f6a-...",
"campaign_run_id": "run_789",
"workspace_id": "ws_01J...",
"total_recipients": 1500,
"sent_count": 1495,
"failed_count": 5,
"skipped_count": 0,
"cancelled_count": 0,
"delivered_count": 1400,
"failed_delivery_count": 95,
"pending_delivery_count": 0,
"delivery_rate": 0.9398,
"failure_rate": 0.0033,
"first_sent_at": "2026-05-12T09:12:01.001Z",
"last_sent_at": "2026-05-12T09:14:48.812Z",
"completed_at": "2026-05-12T09:14:59.044Z"
},
"errors": null,
"request_id": "req_01J..."
}
Field reference
| Field | Type | Notes |
|---|
campaign_id | string (uuid) | The campaign you queried. |
campaign_run_id | string | null | Latest run referenced by the rollup, or null before any run completes. |
workspace_id | string | Workspace that owns the campaign. |
total_recipients | int | Aggregate recipient count across runs. |
sent_count | int | Submit successes. |
failed_count | int | Submit failures. |
skipped_count | int | Recipients skipped before submit. |
cancelled_count | int | Recipients on cancelled runs. |
delivered_count | int | DLR-confirmed deliveries. |
failed_delivery_count | int | DLR-confirmed delivery failures. |
pending_delivery_count | int | Sent but no terminal DLR yet. |
delivery_rate | float | null | delivered_count / sent_count as a 0.0–1.0 float. null before any data. |
failure_rate | float | null | failed_count / total_recipients as a 0.0–1.0 float. null before any data. |
first_sent_at | string | null | ISO 8601 UTC. First successful submit across all runs. |
last_sent_at | string | null | ISO 8601 UTC. Last successful submit across all runs. |
completed_at | string | null | ISO 8601 UTC. When the latest run reached a terminal state. |
Rates are 0.0–1.0 floats — multiply by 100 for display. A delivery_rate of 0.9398 is 93.98%, not 0.94%. Never display the raw value as a percentage. Always check for null before rendering.
Analytics rows are written after the first batch completes. There is a small delay (seconds) between a campaign starting and analytics being non-zero. Do not hit /analytics immediately after /launch and expect populated counters — poll /status until it reports activity, or wait for the terminal transition.
Errors
| HTTP | When |
|---|
| 401 | Missing or invalid X-API-Key. |
| 403 | API key does not own this campaign’s workspace. |
| 404 | campaign_id does not exist. |
| 500 | Server error — safe to retry with backoff. |
Code samples
curl -X GET "https://karibu.briq.tz/v1/campaign/8d2e1f6a-.../analytics" \
-H "X-API-Key: YOUR_API_KEY"
3. Run detail — composite payload
Dashboard. Click any run in the campaign’s “Runs” tab to open a detail modal — the run row, channel + provider + sender metadata, per-batch progress, and a per-recipient list with live message status.
Developer API.
GET https://karibu.briq.tz/v1/campaign/{campaign_id}/runs/{run_id}
Returns the composite “run detail” payload — a single round-trip that gives you everything for a run-detail UI:
| Field | Type | What it contains |
|---|
run | object | The run row — translated status, counters, timestamps. |
campaign_name | string | Display name of the parent campaign. |
channel_name | string | Display name of the channel used (e.g. SMS). |
channel_provider | string | Provider that fulfilled the run (e.g. mock, carrier name). |
snapshot | object | Execution snapshot summary: provider, sender_id, channel, template, personalized, frozen_at. |
batches[] | array | Per-batch progress (see fields below). |
recipients[] | array | Joined recipient list with recipient_status, message_id, message_status, sent_at. Capped at 500 rows. |
recipient_summary | object | { total, pending, sent, delivered, failed } — always accurate even when recipients[] is truncated. |
recipients_truncated | boolean | true when the run has more than 500 recipients and recipients[] was clipped. |
Batch fields
| Field | Type | Notes |
|---|
id | string (uuid) | Batch identifier. |
batch_sequence | int | 1-indexed position within the run. |
status | string | pending, running, completed, failed. |
recipient_count | int | Recipients in this batch. |
sent_count | int | Submit successes within the batch. |
failed_count | int | Submit failures within the batch. |
started_at | string | null | ISO 8601 UTC. |
completed_at | string | null | ISO 8601 UTC. |
error_detail | object | null | Populated when status === "failed". |
Recipient fields
| Field | Type | Notes |
|---|
recipient_id | string | Per-run recipient row id. |
phone_number | string | E.164 digits. |
recipient_status | string | Per-run projection; may lag the live message state by seconds. |
message_id | string | null | The dispatched message; null until submit lands. |
message_status | string | null | Live status from the messages table — the authoritative state of the message right now. |
sent_at | string | null | ISO 8601 UTC. |
recipients[] is capped at 500 rows per response. When more exist, the API sets recipients_truncated: true and clips the array. Use recipient_summary for the aggregate counts — do not derive totals by counting recipients[]. If you need every recipient for a large run, page through the run via the dashboard or contact Briq for a bulk export.
message_status may diverge from recipient_status. The message status is the live truth pulled from the messages table; recipient_status is a slightly stale projection rolled up onto the per-run recipient row. Prefer message_status when you display the current state of a single message.
Errors
| HTTP | When |
|---|
| 401 | Missing or invalid X-API-Key. |
| 403 | API key does not own this campaign’s workspace. |
| 404 | campaign_id does not exist, or run_id does not belong to that campaign. |
| 500 | Server error — safe to retry with backoff. |
Code samples
curl -X GET "https://karibu.briq.tz/v1/campaign/8d2e1f6a-.../runs/run_789" \
-H "X-API-Key: YOUR_API_KEY"
Run snapshot (read-only, PII-gated)
Developer API.
GET https://karibu.briq.tz/v1/campaign/{campaign_id}/runs/{run_id}/snapshot
- Returns the engine’s
execution_snapshot — the frozen, point-in-time state used to dispatch this run: content, sender, channel, provider, cost, recipient count, and optionally the recipient list.
- Query param
?include_recipients=true opts into returning the recipient phone-number list. Stripped by default.
?include_recipients=true returns PII. The recipient list contains phone numbers. Never call this variant from end-user-facing surfaces (e.g. a customer-supportable web app, an unauthenticated dashboard view). Treat it as a server-to-server, admin-tier read. The caller is responsible for handling returned phone numbers under their own compliance regime.
cost is in service units, not currency. total_cost is denominated in currency_units — sms_parts, voice minutes, etc. — matching the units convention used by /validate. Do not display this value as money. Convert to your billing currency client-side if you need to show a price.
Snapshot may be partial before dispatch. For a run still in ready status (queued but not yet picked up by the worker), the engine may not have frozen the full snapshot yet — some fields may be null. Re-read after the run transitions to running or later.
Success response (default, without recipients)
{
"success": true,
"data": {
"run_id": "run_789",
"campaign_id": "8d2e1f6a-...",
"recipient_count": 1500,
"content": {
"channel": "SMS",
"provider": "mock",
"sender_id": "BRIQ",
"body": "Hello, thanks for joining."
},
"cost": {
"total_cost": 1500,
"currency_units": "sms_parts"
},
"status": "running",
"started_at": "2026-05-12T09:12:00.118Z",
"completed_at": null
},
"errors": null,
"request_id": "req_01J..."
}
Success response (with ?include_recipients=true)
The same payload, plus a recipients array of phone numbers (E.164 digits):
{
"success": true,
"data": {
"run_id": "run_789",
"campaign_id": "8d2e1f6a-...",
"recipient_count": 1500,
"content": { "...": "..." },
"cost": { "...": "..." },
"recipients": ["255712345678", "255713000111", "..."],
"status": "running",
"started_at": "2026-05-12T09:12:00.118Z",
"completed_at": null
},
"errors": null,
"request_id": "req_01J..."
}
Errors
| HTTP | When |
|---|
| 401 | Missing or invalid X-API-Key. |
| 403 | API key does not own this campaign’s workspace. |
| 404 | campaign_id does not exist, run_id does not belong to that campaign, or no snapshot has been frozen yet. |
| 500 | Server error — safe to retry with backoff. |
Code samples
# Default — no PII
curl -X GET "https://karibu.briq.tz/v1/campaign/8d2e1f6a-.../runs/run_789/snapshot" \
-H "X-API-Key: YOUR_API_KEY"
# Opt in to the recipient list (server-side only)
curl -X GET "https://karibu.briq.tz/v1/campaign/8d2e1f6a-.../runs/run_789/snapshot?include_recipients=true" \
-H "X-API-Key: YOUR_API_KEY"
What happens after launch
A short narrative of the system behaviour that drives the numbers you see above. Useful if you are building a poller and want to understand the timing.
- Pickup (≤ 30 seconds). A separate worker microservice runs a cron query of
campaign_runs WHERE status = 'scheduled' AND scheduled_at <= now(). It claims the row by flipping its status to the internal claimed sentinel. The API translates claimed to running in every response, so you will only ever see running.
- Batches of 1000. The engine partitions the run’s recipients into batches of up to 1000 and writes a
campaign_batches row per partition. Each batch is dispatched through the campaign’s channel + provider; per-recipient rows land in campaign_recipients and messages.
- Counters roll up. As batches complete,
sent_count, failed_count, skipped_count, and delivered_count on the run row are incremented. The parent campaign’s aggregate counters are recomputed off the run’s totals — this is what populates the next /status and /analytics response.
- Terminal state and analytics. When all batches finish, the run flips to
completed (or failed). The engine then writes a campaign_analytics row — at this point /analytics returns its populated payload.
- Optional refund. If a run failed partway through, or the campaign was cancelled, the engine refunds the unsent portion of the credit reservation. Synchronous on
/cancel; reconciler-driven for other failure paths.
Practical cadence
A 4-bullet recap of what to call when:
- During
scheduled / running: poll /status every 5–15 seconds.
- On terminal transition (
completed, failed, or cancelled): stop polling, call /analytics once for the final rollup.
- For a run-level UI (drilldown modal, per-recipient list): call
/runs/{run_id} for the composite payload. Use the snapshot endpoint only when you need the frozen dispatch state.
- Prefer the dashboard for ad-hoc inspection. Build a poller only when you need automation — e.g. piping data into your own analytics, alerting on failure rates, or driving a custom operator console.