Roko Lucky Drawer — SMS Platform API (0.1.0)

Download OpenAPI specification:

Roko Platform Team: support@robustrade.com

Inbound webhook + outbound send endpoints for the ROKO Nano Fertilizer Lucky Draw Campaign (Guinea, 2026).

This spec is the canonical contract between the Roko AWS backend and the Topaz SMS gateway integration.

  • Inbound webhook (POST /webhook/sms): the payload Topaz MUST POST to us when a farmer sends an SMS. We define this; Topaz implements.
  • Outbound send (POST /sms/send): the internal API our frontend / workers call to send a single-segment SMS.

All production auth tokens are delivered out-of-band via secure channel.

webhook

Inbound SMS from the Topaz gateway.

Receive an inbound SMS from Topaz

Topaz calls this endpoint for every SMS received on our SIM (612 990 119 for stage, 612 987 722 for prod). The request must carry the shared token in X-Roko-Webhook-Token; Roko persists the SMS and returns the internal sms_id.

Authentication

HTTP header X-Roko-Webhook-Token: <token>. The token is compared in constant time against the value Roko stores in SSM at /roko/{stage|prod}/webhook-inbound-token and is issued out-of-band. Missing or wrong token → 401 unauthorized.

Idempotency — important, please read before integrating

The pair (gateway_sms_id, port) is our dedup key. You should send a unique gateway_sms_id per distinct SMS. If you retry the same SMS after a network blip, use the same gateway_sms_id — we will update the existing row in place and return the same sms_id. If you send a different SMS, use a new gateway_sms_id and you'll get a new row and a new sms_id.

Worked example:

Both gateway_sms_id and the returned sms_id are opaque strings (UUIDs recommended). sms_id is a Roko-generated UUID — never sequential, never guessable, safe to log.

Call 1 — farmer sends their first code:

POST /webhook/sms
{"gateway_sms_id": "a1b2c3d4-0001", "port": 17,
 "sender": "612345678", "text": "ROKO U12345678",
 "timestamp": "2026-05-15T10:30:00Z", "source": "topaz"}200  {"status": "accepted",
        "sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11"}

DB: new row; parsed_code=U12345678, public_id=c4f7....

Call 2 — Topaz retries Call 1 because it missed the ACK:

POST /webhook/sms   (identical body)200  {"status": "accepted",
        "sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11"}
        ← same sms_id

DB: still one row; no duplicates.

Call 3 — same farmer sends a second, different SMS. Topaz assigns a new gateway_sms_id:

POST /webhook/sms
{"gateway_sms_id": "a1b2c3d4-0002", "port": 17,
 "sender": "612345678", "text": "ROKO U99999999",
 "timestamp": "2026-05-15T10:35:00Z", "source": "topaz"}200  {"status": "accepted",
        "sms_id": "8f2c1a90-4d5e-4a88-b1ab-12e2a4d55c77"}new sms_id

DB: second row; parsed_code=U99999999.

Call 4 — bug in Topaz: different SMS reuses gateway_sms_id="a1b2c3d4-0001":

POST /webhook/sms
{"gateway_sms_id": "a1b2c3d4-0001", "port": 17,
 "sender": "612345678", "text": "ROKO UXXXXXXXX", ...}200  {"status": "accepted",
        "sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11"}
        ← original sms_id, row overwritten

DB: row from Call 1 is overwritten with the new text + parsed_code. Reusing a gateway_sms_id across different SMSes is a gateway bug that causes data loss on our side.

Notes:

  • sms_id is a UUIDv4 generated by Roko, not a sequential integer. Safe to log, safe to expose in error messages. It's opaque to Topaz — don't try to interpret or correlate it.
  • gateway_sms_id is whatever string Topaz uses (UUID, integer, alphanumeric prefix) up to 64 chars. Integers like 101 are accepted but the JSON-body will be coerced to a string on storage; we recommend using UUIDs for the same privacy reasons that apply to sms_id.
Authorizations:
webhookToken
Request Body schema: application/json
required
required
string or integer

Stable identifier assigned by Topaz for this SMS. May be any format Topaz uses — UUID, alphanumeric, integer — up to 64 chars. Roko stores it as a string. Whatever format is chosen, it MUST be unique per distinct SMS; reuse across different SMSes is a gateway bug (see Idempotency section above).

port
required
integer [ 0 .. 31 ]

SIM slot port on the gateway (0–31). Port identifies the environment.

sender
required
string^[0-9+]{6,20}$

Farmer MSISDN in E.164 format (+224 country code + 9-digit local number). Bare 9-digit local input and 224-prefixed input are also accepted and normalized to E.164 server-side. Stored canonical: +224612345678.

text
required
string [ 1 .. 1600 ] characters

Raw SMS content as received by Topaz. No transformation.

timestamp
required
string <date-time>

ISO-8601 UTC timestamp of when Topaz received the SMS.

source
string <= 30 characters

Vendor identifier. Defaults to "topaz".

Responses

Request samples

Content type
application/json
Example
{
  • "gateway_sms_id": "a1b2c3d4-e5f6-7890-ab12-345678901234",
  • "port": 17,
  • "sender": "612345678",
  • "text": "ROKO U12345678",
  • "timestamp": "2026-05-15T10:30:00Z",
  • "source": "topaz"
}

Response samples

Content type
application/json
{
  • "status": "accepted",
  • "sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11"
}

Delivery-receipt (DLR) webhook from the SMS provider

Inbound webhook used by the active outbound SMS provider (Nimba today) to report status transitions for messages we previously sent via POST /sms/send.

Authentication

The path segment {secret} is itself the credential — a long random URL-safe string stored in SSM at /roko/{stage|prod}/sms-status-webhook-secret. Constant-time compared in code. 401 on mismatch.

Why path-secret rather than a signed header: Nimba's portal does not currently sign payloads. The URL appears once in their dashboard and never in normal logs.

Single-URL routing across environments

Nimba allows only one webhook URL per account, and the same Nimba account serves both stage and prod. We register only prod's URL in their dashboard. If prod receives a DLR whose message ID doesn't match a row in prod's sms_outgoing, prod re-POSTs the raw body to stage's URL with stage's secret. Stage records it as a normal authenticated call. Each env's data stays in its own DB.

Behaviour

  • Every accepted call (path secret matches) is persisted into the sms_status_events audit table verbatim, regardless of whether we can resolve the message ID. Permanent retention.
  • When the message ID resolves to a row in sms_outgoing and the mapped status is delivered or failed, the row's delivery_status is updated. Already-DELIVERED rows are never downgraded.
  • We always return 200 OK after auth — internal lookup or mapping failures never trigger a provider retry.

Real Nimba payload shape will be confirmed against the first live DLR; until then the request body is treated as opaque JSON.

path Parameters
secret
required
string [ 16 .. 128 ] characters

Long URL-safe shared secret. Constant-time compared with the SSM value.

Request Body schema: application/json
optional
property name*
additional property
any

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{
  • "status": "accepted"
}

PBX call-event webhook (Yeastar P-Series)

The Yeastar P560 pushes call lifecycle events here (call start and call end at minimum). Auth is HMAC-SHA256(raw body, shared secret); the verifier is tolerant of several Yeastar firmware variations:

  • Header: X-Signature or X-Signature-256.
  • Value: the raw digest, optionally with a sha256= prefix.
  • Encoding: base64 OR hex — either is accepted.

The signing secret is generated by the PBX (Integrations → API → Webhook Event Push) on a per-webhook-entry basis — each env's Yeastar webhook has its own value, mirrored into the matching SecureString at /roko/{stage,prod}/pbx-webhook-secret. The stage and prod parameters hold different values; each env's Lambda only reads its own.

On success the handler:

  1. Persists every event to call_events (full raw payload kept in JSONB for forensics).
  2. Matches the event's extension to an agents.pbx_extension and that agent's currently IN_PROGRESS callback.
  3. When matched, appends pbx_call_start / pbx_call_end to the callback activity timeline.

Unmatched events are still persisted to call_events (with NULL agent_id / callback_id); they just don't appear on a callback timeline. Auth failures land in webhook_access_failures and the PBX retries per its own retry policy.

The canonical payload shape we operate on internally is { operation: "call_start"|"call_end", extension, call_id, ip_address }, but the handler also normalises several Yeastar aliases (event_type, member_ext, caller_ext, linkedid, etc.) onto that shape.

header Parameters
X-Signature
string

HMAC-SHA256 of the raw body. Optional sha256= prefix; base64 or hex.

X-Signature-256
string

Alias for X-Signature used by some Yeastar firmware versions.

Request Body schema: application/json
required
property name*
additional property
any

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{
  • "status": "accepted",
  • "call_id": "string"
}

send

Outbound SMS originated by Roko.

Send a single-segment SMS via the active provider

Internal endpoint called by the Roko ops dashboard / worker jobs to send an SMS to a farmer.

Provider routing

The active outbound provider is selected at runtime by the SMS_PROVIDER env var on the Lambda (default nimba). Callers DO NOT pick — this is operator-controlled. Each successful response echoes which provider handled the send via the provider field.

Provider-specific rules currently enforced:

  • Nimba (nimba): the text body MUST contain the literal word Roko (case-insensitive). Nimba pairs every send with a registered sender name (Roko); messages without the keyword are silently dropped on their side, so we reject 400 early.

Authentication

HTTP header x-api-key: <key>. The key is issued per caller by the operator and stored in SSM at /roko/{stage|prod}/sender-api-key. Missing or wrong key → 403 Forbidden from API Gateway.

Strict validations (return 400)

  • phone_number must be a Guinea MSISDN. Accepted shapes: +224612345678, 224612345678, or bare local 612345678. The server normalizes to canonical E.164 (+224612345678) before persistence and before handing off to the SMS provider.
  • text must fit in exactly one SMS segment:
    • 160 chars if all characters are in the GSM-7 alphabet
    • 70 chars otherwise (Unicode/UCS-2)
  • Requests exceeding the single-segment limit return 400 with the detected encoding and the applicable limit.

Idempotency — use the Idempotency-Key header

Always generate a fresh UUID per logical send and put it in the Idempotency-Key header. If the request times out or the network drops, retry with the same header value. We will not send a second SMS to the farmer; you get the original sms_id back with the current status.

Format: 8-64 chars from [A-Za-z0-9_.:-]. UUIDv4 (36 chars) is the recommended default.

Worked example:

Call 1 — frontend sends "Merci" to a farmer, with a fresh UUID:

POST /sms/send
Idempotency-Key: 0b3e8bdd-b0ac-4d1a-97f9-3f6f3a3c3e91
x-api-key: <key>
Content-Type: application/json

{"phone_number": "+224612345678",
 "text": "Merci pour votre participation ROKO.",
 "template_key": "VALID_FIRST"}200 {"sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11",
       "status": "SENT",
       "gateway_sms_id": "20260515-topaz-0001"}

DB: new row with client_idempotency_key=0b3e..., public_id=c4f7..., delivery_status=SENT.

Call 2 — frontend hits a read timeout on Call 1 (Topaz was slow) and retries with the same Idempotency-Key:

POST /sms/send
Idempotency-Key: 0b3e8bdd-b0ac-4d1a-97f9-3f6f3a3c3e91
(same body)200 {"sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11",
       "status": "SENT",
       "gateway_sms_id": "20260515-topaz-0001",
       "idempotent_replay": true}

DB: unchanged. Topaz not called a second time. The farmer gets one SMS.

Call 3 — frontend sends a separate message to the same farmer with a new Idempotency-Key:

POST /sms/send
Idempotency-Key: 7d2c8a91-4e56-4f88-9bab-12e2a4d55c77
(different body or same — doesn't matter)200 {"sms_id": "8f2c1a90-4d5e-4a88-b1ab-12e2a4d55c77",
       "status": "SENT",
       "gateway_sms_id": "20260515-topaz-0002"}

DB: second row. Another SMS is sent. This is correct: new key = new logical send.

Call 4 — legacy caller omits the header:

POST /sms/send
(no Idempotency-Key)200 {"sms_id": "...", "status": "SENT", ...}

DB: new row, no dedup. If this caller retries, a duplicate SMS will go out. This is backwards compatible for now but callers should add the header; in a future release we will require it.

Notes:

  • The Idempotency-Key is scoped to the /sms/send endpoint and to this environment. Don't reuse keys across stage and prod.
  • Keys are retained indefinitely; we may add a 30-day TTL later.
  • Don't derive the key from the request body; Call 3 shows why the body is allowed to repeat — the key is what uniquely identifies a send.
Authorizations:
apiKey
header Parameters
Idempotency-Key
string^[A-Za-z0-9_.:\-]{8,64}$
Example: 0b3e8bdd-b0ac-4d1a-97f9-3f6f3a3c3e91

Unique per logical send. Reuse the same value when retrying after a network/timeout failure — we return the prior result without re-sending. Format: 8-64 chars from [A-Za-z0-9_.:-]. Recommended: UUIDv4.

Request Body schema: application/json
required
phone_number
required
string

Guinea MSISDN. Accepts +224612345678, 224612345678, or bare local 612345678 — server normalizes to E.164 (+224612345678) before persistence and before handing off to the SMS provider.

text
required
string non-empty

SMS body. Must fit in one segment (160 GSM-7 or 70 Unicode).

template_key
string <= 30 characters

Optional tag used for analytics (e.g., "VALID_FIRST").

Responses

Request samples

Content type
application/json
{
  • "phone_number": "+224612345678",
  • "text": "Merci pour votre participation ROKO.",
  • "template_key": "VALID_FIRST"
}

Response samples

Content type
application/json
{
  • "sms_id": "92ddb31d-ddfe-4c89-98f6-b5cabf49fc53",
  • "status": "QUEUED",
  • "provider": "nimba",
  • "gateway_sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11",
  • "reason": "string",
  • "idempotent_replay": true
}

admin

Phase 3 admin console API. Cognito-authenticated, restricted to the campaign-admin group. The Next.js admin frontend (apps/frontend-admin) is the only known consumer. All mutating actions are recorded in admin_audit_log.

List the in-code rule TYPE catalog

Authorizations:
cognitoUserPool

Responses

Response samples

Content type
application/json
{
  • "rule_types": [
    ]
}

Paginated inbound-SMS log with audit trail (rule matched + reply sent)

Returns every inbound SMS joined to the reply (if any), the matched campaign rule, the campaign, and the code's status in the master. parsed_code is masked (U1•••••78) — the full code never leaves the Lambda.

Authorizations:
cognitoUserPool
query Parameters
limit
integer [ 1 .. 200 ]
Default: 50
offset
integer >= 0
Default: 0
campaign_id
string <uuid>

Campaign public_id; omit for cross-campaign view

sender_phone
string

Substring (ILIKE)

parsed_code
string

Substring on the full code (server-side; the response masks)

matched_rule_type
string
Enum: "NOT_GUINEA_NUMBER" "BLOCKLIST" "INVALID_FORMAT" "PRE_CAMPAIGN_SMS" "POST_CAMPAIGN_SMS" "INVALID_CODE" "DUPLICATE_CODE" "MAX_ENTRIES" "BATCH_NOT_ACTIVATED" "PHONE_BULK_DETECTION" "ZENDESK_FIRST_TICKET" "VALID_REPEAT" "VALID_FIRST_TIME" "SYSTEM_ERROR_FALLBACK"
template_key
string
Enum: "A" "B" "C" "D" "E" "F" "G" "H" "H_EARLY" "POST_END"
delivery_status
string
Enum: "QUEUED" "SENT" "DELIVERED" "FAILED"
silent
string
Enum: "true" "false"
received_after
string <date-time>
received_before
string <date-time>

Responses

Response samples

Content type
application/json
{
  • "total": 0,
  • "limit": 0,
  • "offset": 0,
  • "messages": [
    ]
}

Headline KPIs + 14-day timeseries for the manager portal

Read-only aggregation for the P4-A manager portal. Group requirement: manager or campaign-admin (admins inherit). SMS metrics are scoped to the resolved campaign; callback and fraud metrics are scoped by date only (no campaign FK in the schema). Simulated rows (is_simulated = true) are excluded.

Authorizations:
cognitoUserPool
query Parameters
campaign_id
string <uuid>

Campaign public_id; defaults to the ACTIVE campaign

from
string <date>

Inclusive lower bound (YYYY-MM-DD); default 14 days ago

to
string <date>

Inclusive upper bound (YYYY-MM-DD); default today

Responses

Response samples

Content type
application/json
{ }

SMS performance KPIs (SOP 10.4 A)

Totals, product split (Urea/DAP), region breakdown, the received→valid→first-time→registered funnel, and a daily timeseries. Same auth + scoping as /admin/analytics/overview.

Authorizations:
cognitoUserPool
query Parameters
campaign_id
string <uuid>
from
string <date>
to
string <date>

Responses

Response samples

Content type
application/json
{ }

Call-center KPIs (SOP 10.4 B)

Callback counts by status, 24h-SLA compliance, average time-to-first-attempt, the disposition table, and a per-agent rollup. Scoped by date only. Same auth as the other analytics endpoints.

Authorizations:
cognitoUserPool
query Parameters
from
string <date>
to
string <date>

Responses

Response samples

Content type
application/json
{ }

Fraud & risk KPIs (SOP 10.4 E)

Flags raised, flags closed within the 48h SLA, open flags, blocked numbers by reason, and a daily timeseries. Scoped by date only.

Authorizations:
cognitoUserPool
query Parameters
from
string <date>
to
string <date>

Responses

Response samples

Content type
application/json
{ }

Week-over-week management summary

Each headline metric for the current ISO week vs the previous ISO week (Monday-aligned, UTC), with absolute + percentage delta. Feeds the weekly management summary view (SOP 10.5).

Authorizations:
cognitoUserPool
query Parameters
campaign_id
string <uuid>

Responses

Response samples

Content type
application/json
{ }

List draws

Returns up to 500 draw rows ordered by most recent first. Optional filters: campaign_id (UUID), region, status, simulated (true/false). campaign-admin only.

Authorizations:
cognitoUserPool
query Parameters
campaign_id
string <uuid>
region
string
status
string
Enum: "PENDING" "VERIFIED" "ANNOUNCED" "HANDED_OVER" "DISPUTED"
simulated
string
Enum: "true" "false"

Responses

Run a draw for one region (idempotent)

Picks 2 * quota farmers via weighted random sampling without replacement; first quota become primaries (rank 1..N), next quota become alternates. UNIQUE (campaign_id, draw_cycle, region, prize_rank, is_simulated) makes re-submission a no-op — the existing rows are returned with idempotent_replay: true.

Audit: seed (sha256 of inputs) + full eligible pool + ordered picks stored in winner_picks JSONB on every row, together with the campaign's participant_scope and the resolved entries_since cutoff active at execution time (Phase 4-F).

The pool honours campaigns.draw_participant_scope — see the Campaign schema for the two modes. Previous non-disqualified primary winners are always excluded.

On stage with simulated: true, only is_simulated=true farmers are drawn from; on prod the same request returns 422.

Authorizations:
cognitoUserPool
Request Body schema: application/json
required
campaign_id
required
string <uuid>
draw_cycle
required
string^[A-Za-z0-9_.:-]{2,30}$
region
required
string
simulated
boolean
Default: false
nonce
string or null

Responses

Request samples

Content type
application/json
{
  • "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
  • "draw_cycle": "2026-W21",
  • "region": "Boké",
  • "simulated": false,
  • "nonce": "string"
}

Preview the eligible pool for a draw (no write)

Same eligibility filter the draw uses, but returns counts + total weight + a sample of up to 20 farmers for UI preview. The response echoes the campaign's active participant_scope (ALL | SINCE_LAST_DRAW) and the resolved entries_since cutoff (previous draw's execution time for this region, or null when the whole campaign history counts) so the operator can verify the pool before running. campaign-admin only.

Authorizations:
cognitoUserPool
query Parameters
campaign_id
required
string <uuid>
region
required
string
simulated
string
Default: "false"
Enum: "true" "false"

Responses

Get a single draw (one prize rank)

Authorizations:
cognitoUserPool
path Parameters
public_id
required
string <uuid>

Responses

Move a draw's status forward

Allowed transitions: PENDING → VERIFIED|DISPUTED, VERIFIED → ANNOUNCED|DISPUTED, ANNOUNCED → HANDED_OVER|DISPUTED, HANDED_OVER → DISPUTED, DISPUTED → VERIFIED|ANNOUNCED|HANDED_OVER. Invalid transitions return 409. Disqualification is a separate endpoint.

Authorizations:
cognitoUserPool
path Parameters
public_id
required
string <uuid>
Request Body schema: application/json
required
status
required
string
Enum: "PENDING" "VERIFIED" "ANNOUNCED" "HANDED_OVER" "DISPUTED"
notes
string or null

Responses

Request samples

Content type
application/json
{
  • "status": "PENDING",
  • "notes": "string"
}

Disqualify a primary and promote the alternate

Promotes the current alternate to primary, picks a new alternate from the frozen winner_picks list (next unused farmer ID that isn't a current primary/alternate on any sibling rank), and resets status to PENDING for fresh verification. Audit-logged. One-way per row.

Authorizations:
cognitoUserPool
path Parameters
public_id
required
string <uuid>
Request Body schema: application/json
required
reason
required
string >= 3 characters

Responses

Request samples

Content type
application/json
{
  • "reason": "string"
}

Seed N synthetic farmers per region for draw rehearsals (stage only)

Creates count farmers in EACH region of regions[], all with is_simulated=true, status REGISTERED, random phone, random demographics, and entry_count randomly distributed in [entry_count_min, entry_count_max]. Total rows created = count * regions.length. One outer transaction wraps every region — a failure mid-array rolls all of them back. Returns 403 on prod. campaign-admin only.

Legacy single-region form ({ region: "X" }) is still accepted for backwards compat.

Authorizations:
cognitoUserPool
Request Body schema: application/json
required
count
required
integer [ 1 .. 5000 ]

Per region.

regions
required
Array of strings non-empty
entry_count_min
integer [ 1 .. 24 ]
Default: 1
entry_count_max
integer [ 1 .. 24 ]
Default: 24

Responses

Request samples

Content type
application/json
{
  • "count": 1,
  • "regions": [
    ],
  • "entry_count_min": 1,
  • "entry_count_max": 24
}

Wipe all simulated farmers + simulated draw rows (stage only)

Authorizations:
cognitoUserPool

Responses

List portal-access grants

DB-driven authorization grants. authorize() resolves a person's effective roles as the union of these rows (keyed by email) and the legacy cognito:groups claim, so a granted person lands on their dashboard on the very first Google sign-in — no CLI step. campaign-admin only.

Authorizations:
cognitoUserPool

Responses

Response samples

Content type
application/json
{ }

Grant portal access to an email

Creates (or reactivates) a grant. pbx_extension is only valid for role: agent and must be unique across agents + grants.

Authorizations:
cognitoUserPool
Request Body schema: application/json
required
email
required
string <email>
role
required
string
Enum: "campaign-admin" "supervisor" "agent" "manager"
pbx_extension
string or null^\d{1,8}$

Responses

Request samples

Content type
application/json
{
  • "email": "user@example.com",
  • "role": "campaign-admin",
  • "pbx_extension": "string"
}

Response samples

Content type
application/json
{ }

Update a grant (toggle active, set pbx_extension)

Authorizations:
cognitoUserPool
path Parameters
id
required
string <uuid>

Grant public_id

Request Body schema: application/json
required
active
boolean
pbx_extension
string or null^\d{1,8}$

Responses

Request samples

Content type
application/json
{
  • "active": true,
  • "pbx_extension": "string"
}

Response samples

Content type
application/json
{ }

Revoke (delete) a grant

Authorizations:
cognitoUserPool
path Parameters
id
required
string <uuid>

Grant public_id

Responses

Response samples

Content type
application/json
{
  • "ok": true
}

List campaigns

Authorizations:
cognitoUserPool

Responses

Response samples

Content type
application/json
{
  • "campaigns": [
    ]
}

Create a campaign

Authorizations:
cognitoUserPool
Request Body schema: application/json
required
public_id
string <uuid>
code
string
name
string
description
string or null
start_date
string <date>
end_date
string <date>
sender_phone
string
status
string
Enum: "DRAFT" "ACTIVE" "PAUSED" "CLOSED"
object

Per-région motorcycle quota (Phase 4-B). Keys MUST be in the canonical 8-region set; values must be non-negative integers. Missing region keys mean 0 prizes there — POST /admin/draws refuses to run in regions with 0 quota.

draw_participant_scope
string
Default: "ALL"
Enum: "ALL" "SINCE_LAST_DRAW"

Which farmers a draw pools (Phase 4-F). ALL — every REGISTERED farmer with entry_count > 0, weighted by lifetime entry_count (historic behaviour). SINCE_LAST_DRAW — only farmers with new valid entries recorded after the previous draw of the same (campaign, region, sim flag), weighted by the count of those new entries; a region's first draw has no cutoff. In both modes a current non-disqualified primary winner is excluded — "the same farmer can never win twice" is not configurable. Changing the setting affects future draws only; each executed draw freezes the scope + resolved cutoff into its winner_picks audit.

created_by
string or null
created_at
string <date-time>
updated_at
string <date-time>

Responses

Request samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "code": "roko-super-lot-2026",
  • "name": "string",
  • "description": "string",
  • "start_date": "2019-08-24",
  • "end_date": "2019-08-24",
  • "sender_phone": "612987722",
  • "status": "DRAFT",
  • "prize_quotas": {
    },
  • "draw_participant_scope": "ALL",
  • "created_by": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "code": "roko-super-lot-2026",
  • "name": "string",
  • "description": "string",
  • "start_date": "2019-08-24",
  • "end_date": "2019-08-24",
  • "sender_phone": "612987722",
  • "status": "DRAFT",
  • "prize_quotas": {
    },
  • "draw_participant_scope": "ALL",
  • "created_by": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Fetch a campaign

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "code": "roko-super-lot-2026",
  • "name": "string",
  • "description": "string",
  • "start_date": "2019-08-24",
  • "end_date": "2019-08-24",
  • "sender_phone": "612987722",
  • "status": "DRAFT",
  • "prize_quotas": {
    },
  • "draw_participant_scope": "ALL",
  • "created_by": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Update a campaign (status, dates, name, sender_phone, description, prize_quotas, draw_participant_scope)

Allowed patch fields: name, description, start_date, end_date, sender_phone, status, prize_quotas, draw_participant_scope. Other fields are silently ignored. prize_quotas is validated against the canonical 8-region set; values must be non-negative integers ≤ 1000. draw_participant_scope must be ALL or SINCE_LAST_DRAW.

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
Request Body schema: application/json
required
property name*
additional property
any

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "code": "roko-super-lot-2026",
  • "name": "string",
  • "description": "string",
  • "start_date": "2019-08-24",
  • "end_date": "2019-08-24",
  • "sender_phone": "612987722",
  • "status": "DRAFT",
  • "prize_quotas": {
    },
  • "draw_participant_scope": "ALL",
  • "created_by": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

List rules for a campaign

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "campaign_code": "string",
  • "rules": [
    ]
}

Add a rule to a campaign (DRAFT)

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
Request Body schema: application/json
required
rule_type
required
string
object
priority
integer
notes
string

Responses

Request samples

Content type
application/json
{
  • "rule_type": "string",
  • "params": { },
  • "priority": 0,
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "rule_type": "MAX_ENTRIES",
  • "params": { },
  • "priority": 80,
  • "status": "DRAFT",
  • "scenarios_passed_at": "2019-08-24T14:15:22Z",
  • "tc_updated_at": "2019-08-24T14:15:22Z",
  • "enabled_at": "2019-08-24T14:15:22Z",
  • "notes": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Update a rule's params, priority, or notes (resets workflow gate)

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
rid
required
string <uuid>
Request Body schema: application/json
required
property name*
additional property
any

Responses

Request samples

Content type
application/json
{ }

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "rule_type": "MAX_ENTRIES",
  • "params": { },
  • "priority": 80,
  • "status": "DRAFT",
  • "scenarios_passed_at": "2019-08-24T14:15:22Z",
  • "tc_updated_at": "2019-08-24T14:15:22Z",
  • "enabled_at": "2019-08-24T14:15:22Z",
  • "notes": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Delete a rule

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
rid
required
string <uuid>

Responses

Enable a rule (workflow gate enforced)

Returns 409 with error: scenarios_not_passed or tc_not_updated if the 4-step workflow has not been satisfied.

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
rid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "rule_type": "MAX_ENTRIES",
  • "params": { },
  • "priority": 80,
  • "status": "DRAFT",
  • "scenarios_passed_at": "2019-08-24T14:15:22Z",
  • "tc_updated_at": "2019-08-24T14:15:22Z",
  • "enabled_at": "2019-08-24T14:15:22Z",
  • "notes": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Disable a rule

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
rid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "rule_type": "MAX_ENTRIES",
  • "params": { },
  • "priority": 80,
  • "status": "DRAFT",
  • "scenarios_passed_at": "2019-08-24T14:15:22Z",
  • "tc_updated_at": "2019-08-24T14:15:22Z",
  • "enabled_at": "2019-08-24T14:15:22Z",
  • "notes": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

List scenarios for a rule

Authorizations:
cognitoUserPool
path Parameters
rid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "rule_type": "string",
  • "scenarios": [
    ]
}

Add a scenario to a rule

Authorizations:
cognitoUserPool
path Parameters
rid
required
string <uuid>
Request Body schema: application/json
required
public_id
string <uuid>
name
string
sender_phone
string
message_text
string
simulated_now
string or null <date-time>
object
expected_rule_type
string
expected_reply_template
string or null
expected_silent
boolean
last_run_at
string or null <date-time>
last_run_passed
boolean or null
last_run_actual_rule
string or null
last_run_actual_reply
string or null
object or null

Responses

Request samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "name": "string",
  • "sender_phone": "string",
  • "message_text": "string",
  • "simulated_now": "2019-08-24T14:15:22Z",
  • "fixtures": { },
  • "expected_rule_type": "string",
  • "expected_reply_template": "string",
  • "expected_silent": true,
  • "last_run_at": "2019-08-24T14:15:22Z",
  • "last_run_passed": true,
  • "last_run_actual_rule": "string",
  • "last_run_actual_reply": "string",
  • "last_run_trace": { }
}

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "name": "string",
  • "sender_phone": "string",
  • "message_text": "string",
  • "simulated_now": "2019-08-24T14:15:22Z",
  • "fixtures": { },
  • "expected_rule_type": "string",
  • "expected_reply_template": "string",
  • "expected_silent": true,
  • "last_run_at": "2019-08-24T14:15:22Z",
  • "last_run_passed": true,
  • "last_run_actual_rule": "string",
  • "last_run_actual_reply": "string",
  • "last_run_trace": { }
}

Delete a scenario

Authorizations:
cognitoUserPool
path Parameters
rid
required
string <uuid>
sid
required
string <uuid>

Responses

Re-run every scenario for the rule (dryRun) and update last_run_*

Authorizations:
cognitoUserPool
path Parameters
rid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "rule_type": "string",
  • "all_passed": true,
  • "results": [
    ]
}

Dry-run an SMS through the rules engine

Runs the entire campaign rules pipeline against the supplied message in a transaction that is always rolled back. No DB writes persist; no outbound SMS is sent. Returns the matched rule, the reply that would have been sent, and the full rule trace.

Authorizations:
cognitoUserPool
Request Body schema: application/json
required
campaign_code
required
string
sender_phone
required
string
message_text
required
string
simulated_now
string <date-time>
object

Responses

Request samples

Content type
application/json
{
  • "campaign_code": "roko-super-lot-2026",
  • "sender_phone": "+224612000001",
  • "message_text": "ROKO U12345678",
  • "simulated_now": "2019-08-24T14:15:22Z",
  • "fixtures": { }
}

Response samples

Content type
application/json
{
  • "ok": true,
  • "campaign": {
    },
  • "matchedRule": {
    },
  • "replyTemplateKey": "string",
  • "replyText": "string",
  • "silent": true,
  • "trace": [
    ],
  • "sideEffects": [
    ],
  • "dryRun": true
}

List blocked numbers

Authorizations:
cognitoUserPool
query Parameters
limit
integer <= 500
Default: 200
offset
integer
Default: 0
campaign_id
integer

Responses

Response samples

Content type
application/json
{
  • "entries": [
    ],
  • "total": 0,
  • "limit": 0,
  • "offset": 0
}

Add (or upsert) a blocklist entry

Authorizations:
cognitoUserPool
Request Body schema: application/json
required
phone_number
required
string
reason
required
string
campaign_id
integer or null
object

Responses

Request samples

Content type
application/json
{
  • "phone_number": "string",
  • "reason": "string",
  • "campaign_id": 0,
  • "metadata": { }
}

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "phone_number": "+224612000001",
  • "reason": "EMPLOYEE",
  • "campaign_id": 0,
  • "imported_from": "string",
  • "metadata": { },
  • "created_at": "2019-08-24T14:15:22Z"
}

Delete a blocklist entry

Authorizations:
cognitoUserPool
path Parameters
id
required
string <uuid>

Responses

List all T&C versions for a campaign

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "campaign_code": "string",
  • "versions": [
    ]
}

Create a new T&C draft version (does not publish)

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
Request Body schema: application/json
required
language
required
string
Enum: "EN" "FR"
body_md
required
string

Responses

Request samples

Content type
application/json
{
  • "language": "EN",
  • "body_md": "string"
}

Response samples

Content type
application/json
{
  • "public_id": "eb3ce6c7-ce7e-4638-a072-e5054706f601",
  • "language": "EN",
  • "version": 0,
  • "body_md": "string",
  • "published_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

Publish a T&C version (resets tc_updated_at on every rule of the campaign)

Authorizations:
cognitoUserPool
path Parameters
cid
required
string <uuid>
lang
required
string
Enum: "EN" "FR"
Request Body schema: application/json
required
version
required
integer >= 1

Responses

Request samples

Content type
application/json
{
  • "version": 1
}