Skip to main content
Once your campaign has audiences, content, and at least one run, you can validate it, launch it, and control its in-flight state. The dashboard exposes all of this through the campaign view’s action buttons; the developer API exposes the same operations as a small set of POST endpoints. This page covers:
  • Validation (pre-flight check)
  • Launch (go live)
  • Runtime controls — pause, resume, cancel
  • Retry (rerun a failed or cancelled run on the still-pending recipients)

1. Validate (pre-flight)

Dashboard: sign in at https://briq.tz/login, open the campaign, and click Validate in the action bar. The UI shows green for a clean validation, or a list of issues to fix. The Launch button runs the same checks automatically, so most teams only invoke Validate explicitly while iterating on content or audience filters. Developer API: POST /v1/campaign/{campaign_id}/validate The endpoint is read-only — it does not mutate campaign state, so it is safe to call repeatedly (for example, from a “Looks good?” preview screen in your own admin tool).
Validation failures do NOT return an HTTP error. This endpoint always returns 200 OK. A failed validation is signalled by data.valid: false and a populated data.issues[] array. Branch on body.data.valid, never on the HTTP status.

Success response (valid)

{
  "success": true,
  "data": {
    "valid": true,
    "issues": [],
    "estimated_cost": { "SMS": 1500 }
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}

Response when invalid

Same envelope, with data.valid: false and human-readable strings in data.issues[]:
{
  "success": true,
  "data": {
    "valid": false,
    "issues": [
      "Audience is empty",
      "Run 'run_789' has no scheduled_at"
    ],
    "estimated_cost": {}
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}
estimated_cost is in service units, not currency. The map is keyed by channel and reports SMS parts, voice minutes, or push notifications — never a money amount. Do not render these numbers with a currency symbol. To show a monetary estimate, multiply by your account’s per-unit rates client-side.

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENAPI key does not own this campaign.
404NOT_FOUNDcampaign_id does not exist.
500INTERNAL_ERRORUnhandled server error.
No 400 or 409 originates from /validate itself — surface-level rejections (auth, scope, existence) only.

Code samples

curl -X POST https://karibu.briq.tz/v1/campaign/camp_123/validate \
  -H "X-API-Key: YOUR_API_KEY"

2. Launch

Dashboard: on the campaign view at https://briq.tz/login, click Launch. The dashboard auto-validates the campaign, marks it ready, and triggers the earliest pending run. If validation fails, you see the same issue list as the Validate panel — fix the issues and click Launch again. Developer API: POST /v1/campaign/{campaign_id}/launch Launch is a composite operation: it validates the campaign, marks it ready, and triggers a run in one call.

Request body

FieldTypeRequiredNotes
run_idstringnoTrigger a specific run. Omit to auto-pick the earliest eligible run.
When run_id is omitted, the server selects the earliest run whose status is "ready", ordered by scheduled_at ascending with nulls last.

Success response

Carries the triggered run with status: "scheduled". The parent campaign flips to scheduled.
{
  "success": true,
  "data": {
    "run_id": "run_789",
    "campaign_id": "camp_123",
    "status": "scheduled",
    "scheduled_at": "2026-05-12T14:00:00.000000",
    "recipient_count": 1500
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}
After launch returns, the worker’s pickup cron claims the run on its next tick — typically within seconds — and begins dispatching in batches of 1000 recipients.
Recurring template runs. For a recurring template run (one where recurrence_index is null), launch materialises the child occurrences and retires the template. From that point on, each occurrence has its own run_id and lifecycle.

Errors

StatusCodeWhen
400VALIDATION_FAILEDNo ready run is available: "No 'ready' run available for this campaign".
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENAPI key does not own this campaign.
404NOT_FOUNDcampaign_id does not exist, or run_id does not belong to it.
409CONFLICTValidation failed at launch time. See special envelope below.
409CONFLICTThe targeted run is already in running, completed, failed, or cancelled and cannot be re-triggered.
500INTERNAL_ERRORUnhandled server error.
Validation-failed 409 uses a special dual envelope. Both data and errors are populated — the validation result lives in data.issues[], and errors[] is just the protocol-level marker.
{
  "success": false,
  "data": {
    "valid": false,
    "issues": ["Audience is empty"],
    "estimated_cost": {}
  },
  "errors": [
    { "code": "CONFLICT", "message": "Validation failed", "field": null }
  ],
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}
Inspect data.issues[] — that’s where each failure shows up. Do not parse errors[0].message for the user-facing reason.

Code samples

curl -X POST https://karibu.briq.tz/v1/campaign/camp_123/launch \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

3. Pause and resume

Dashboard: on the running campaign view at https://briq.tz/login, use the Pause and Resume buttons. Pause halts in-flight runs after the current batch; Resume flips them back to scheduled and reschedules them for immediate pickup.

Pause

POST /v1/campaign/{campaign_id}/pause Stops in-flight runs mid-flight. Internally, the server sets a Redis flag and flips active runs — those in running or the internal claimed state — to paused. The endpoint is idempotent: calling pause on an already-paused campaign returns 200 OK. Runs that were scheduled or ready at pause time are not touched — they remain dormant on their own schedule and will pick up normally unless you also cancel.
Pause is not instantaneous. The worker finishes the current 1000-recipient batch before halting. After the pause call returns, you may still see a few hundred additional sent_count reach recipients. Do not rely on pause as a “stop sending now” guarantee — for that, use cancel.

Success response

{
  "success": true,
  "data": {
    "campaign_id": "camp_123",
    "action": "paused",
    "status": "paused"
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENAPI key does not own this campaign.
404NOT_FOUNDcampaign_id does not exist.
500INTERNAL_ERRORUnhandled server error.
No 400 or 409 from this endpoint — pause is always accepted on a non-terminal campaign.

Code samples

curl -X POST https://karibu.briq.tz/v1/campaign/camp_123/pause \
  -H "X-API-Key: YOUR_API_KEY"

Resume

POST /v1/campaign/{campaign_id}/resume Clears the pause flag and flips paused runs back to scheduled. The endpoint is idempotent: calling resume on a campaign that was never paused is a no-op (the Redis pause key is deleted whether it existed or not).
Resume is “go-now” — the original scheduled_at is NOT preserved. Resume rewrites the run’s scheduled_at to the current time so the worker picks it up on the next tick.The rationale: a run paused at noon yesterday with a scheduled_at of noon yesterday would otherwise sit idle until noon tomorrow, because the worker only claims runs whose scheduled_at is in the past but within the pickup window. Go-now is the only safe resume behaviour.If you need to honour the original schedule, save it client-side before pausing and re-PATCH it after resume.

Success response

{
  "success": true,
  "data": {
    "campaign_id": "camp_123",
    "action": "resumed",
    "status": "scheduled"
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENAPI key does not own this campaign.
404NOT_FOUNDcampaign_id does not exist.
409CONFLICTCampaign is cancelled — sticky, cannot be resumed.
500INTERNAL_ERRORUnhandled server error.

Code samples

curl -X POST https://karibu.briq.tz/v1/campaign/camp_123/resume \
  -H "X-API-Key: YOUR_API_KEY"

4. Cancel

Dashboard: on the campaign view at https://briq.tz/login, click Cancel campaign. A confirmation dialog warns that cancellation is permanent before the action commits. Developer API: POST /v1/campaign/{campaign_id}/cancel
Cancel is sticky — there is no way back. Once a campaign is cancelled, none of pause, resume, launch, retry, or PATCH will revive it. To send the remaining recipients you must either retry the individual cancelled runs (see the next section) or create a new campaign.

What happens internally

  1. Sets the Redis cancel flag so the worker stops claiming new batches.
  2. Flips every non-terminal run (ready, scheduled, claimed, running, paused) to cancelled.
  3. Marks the parent campaign cancelled.
  4. Refunds unsent reservations synchronously. Failures here are logged and retried by an out-of-band reconciler cron — the campaign stays cancelled either way.
Messages already dispatched before cancel cannot be unsent. Only the unsent reservation portion is refunded.

Success response

{
  "success": true,
  "data": {
    "campaign_id": "camp_123",
    "action": "cancelled",
    "status": "cancelled",
    "runs_cancelled": 3
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}

Errors

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENAPI key does not own this campaign.
404NOT_FOUNDcampaign_id does not exist.
500INTERNAL_ERRORUnhandled server error.

Code samples

curl -X POST https://karibu.briq.tz/v1/campaign/camp_123/cancel \
  -H "X-API-Key: YOUR_API_KEY"

5. Retry a failed or cancelled run

Dashboard: at https://briq.tz/login, open the run’s detail view (any run in failed or cancelled state) and click Retry run. The dashboard skips recipients whose messages already reached SENT or DELIVERED and reruns the rest. Developer API: POST /v1/campaign/{campaign_id}/runs/{run_id}/retry Retry creates a new run row with a new id, the same content, and a filtered recipient list. The new run is created in scheduled state with scheduled_at = now(), so the worker’s next pickup tick claims it.
The returned run_id is new — not the original. Keep both for audit. The new run’s snapshot includes a retry_of_run_id field pointing back at the source run.

Filter mechanism

The retry recipient list is built by joining campaign_recipients.provider_message_id to messages.message_id. Any recipient whose latest message status is SENT or DELIVERED is excluded from the retry. Everything else — PENDING, FAILED, never-dispatched — is included.

Success response

Same shape as /launch:
{
  "success": true,
  "data": {
    "run_id": "run_790",
    "campaign_id": "camp_123",
    "status": "scheduled",
    "scheduled_at": "2026-05-12T14:32:11.000000",
    "retry_of_run_id": "run_789",
    "recipient_count": 412
  },
  "errors": null,
  "request_id": "req_01HXXXXXXXXXXXXXXXXXXXXXXX"
}

Errors

StatusCodeWhen
400VALIDATION_FAILEDSource run is not failed or cancelled: "Only FAILED or CANCELLED runs can be retried. Current status: '<x>'".
400VALIDATION_FAILEDEvery recipient on the source run was already SENT or DELIVERED — nothing left to retry.
401UNAUTHORIZEDMissing or invalid API key.
403FORBIDDENAPI key does not own this campaign.
404NOT_FOUNDcampaign_id does not exist, or run_id does not belong to it.
500INTERNAL_ERRORUnhandled server error.

Code samples

curl -X POST https://karibu.briq.tz/v1/campaign/camp_123/runs/run_789/retry \
  -H "X-API-Key: YOUR_API_KEY"

Quick reference

OperationEndpointReversible?Side effects
ValidatePOST .../validaten/a — read-onlyNone
LaunchPOST .../launchNo (terminal once running)Validates, marks ready, triggers a run
PausePOST .../pauseYes (via resume)Stops in-flight runs after current batch
ResumePOST .../resumen/aResets scheduled_at to now — go-now semantics
CancelPOST .../cancelNo — stickyRefunds unsent reservations
RetryPOST .../runs/{run_id}/retryn/a — creates a new runNew run id, filters out already-sent recipients

Next