Download OpenAPI specification:
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.
POST /webhook/sms): the payload Topaz MUST
POST to us when a farmer sends an SMS. We define this; Topaz
implements.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.
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.
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.
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.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 ( |
| 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". |
{- "gateway_sms_id": "a1b2c3d4-e5f6-7890-ab12-345678901234",
- "port": 17,
- "sender": "612345678",
- "text": "ROKO U12345678",
- "timestamp": "2026-05-15T10:30:00Z",
- "source": "topaz"
}{- "status": "accepted",
- "sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11"
}Inbound webhook used by the active outbound SMS provider (Nimba
today) to report status transitions for messages we previously
sent via POST /sms/send.
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.
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.
sms_status_events audit table verbatim, regardless of whether
we can resolve the message ID. Permanent retention.sms_outgoing and the
mapped status is delivered or failed, the row's
delivery_status is updated. Already-DELIVERED rows are
never downgraded.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.
| secret required | string [ 16 .. 128 ] characters Long URL-safe shared secret. Constant-time compared with the SSM value. |
| property name* additional property | any |
{ }{- "status": "accepted"
}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:
X-Signature or X-Signature-256.sha256= prefix.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:
call_events (full raw payload kept
in JSONB for forensics).agents.pbx_extension
and that agent's currently IN_PROGRESS callback.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.
| X-Signature | string HMAC-SHA256 of the raw body. Optional |
| X-Signature-256 | string Alias for |
| property name* additional property | any |
{ }{- "status": "accepted",
- "call_id": "string"
}Internal endpoint called by the Roko ops dashboard / worker jobs to send an SMS to a farmer.
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): 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.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.
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:400 with the
detected encoding and the applicable limit.Idempotency-Key headerAlways 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:
Idempotency-Key is scoped to the /sms/send endpoint and
to this environment. Don't reuse keys across stage and prod.| 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 |
| phone_number required | string Guinea MSISDN. Accepts |
| 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"). |
{- "phone_number": "+224612345678",
- "text": "Merci pour votre participation ROKO.",
- "template_key": "VALID_FIRST"
}{- "sms_id": "92ddb31d-ddfe-4c89-98f6-b5cabf49fc53",
- "status": "QUEUED",
- "provider": "nimba",
- "gateway_sms_id": "c4f7e2a0-3b1e-4d2a-9e18-1a6e5f8c0c11",
- "reason": "string",
- "idempotent_replay": true
}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.
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.
| 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> |
{- "total": 0,
- "limit": 0,
- "offset": 0,
- "messages": [
- { }
]
}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.
| 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 |
{ }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.
| campaign_id | string <uuid> |
| from | string <date> |
| to | string <date> |
{ }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.
| from | string <date> |
| to | string <date> |
{ }Flags raised, flags closed within the 48h SLA, open flags, blocked numbers by reason, and a daily timeseries. Scoped by date only.
| from | string <date> |
| to | string <date> |
{ }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).
| campaign_id | string <uuid> |
{ }Returns up to 500 draw rows ordered by most recent first. Optional
filters: campaign_id (UUID), region, status, simulated
(true/false). campaign-admin only.
| campaign_id | string <uuid> |
| region | string |
| status | string Enum: "PENDING" "VERIFIED" "ANNOUNCED" "HANDED_OVER" "DISPUTED" |
| simulated | string Enum: "true" "false" |
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.
| 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 |
{- "campaign_id": "78614b6c-fe7c-41e2-8e25-c9b3a3c91904",
- "draw_cycle": "2026-W21",
- "region": "Boké",
- "simulated": false,
- "nonce": "string"
}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.
| campaign_id required | string <uuid> |
| region required | string |
| simulated | string Default: "false" Enum: "true" "false" |
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.
| public_id required | string <uuid> |
| status required | string Enum: "PENDING" "VERIFIED" "ANNOUNCED" "HANDED_OVER" "DISPUTED" |
| notes | string or null |
{- "status": "PENDING",
- "notes": "string"
}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.
| public_id required | string <uuid> |
| reason required | string >= 3 characters |
{- "reason": "string"
}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.
| 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 |
{- "count": 1,
- "regions": [
- "string"
], - "entry_count_min": 1,
- "entry_count_max": 24
}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.
{ }Creates (or reactivates) a grant. pbx_extension is only valid for
role: agent and must be unique across agents + grants.
| email required | string <email> |
| role required | string Enum: "campaign-admin" "supervisor" "agent" "manager" |
| pbx_extension | string or null^\d{1,8}$ |
{- "email": "user@example.com",
- "role": "campaign-admin",
- "pbx_extension": "string"
}{ }| id required | string <uuid> Grant public_id |
| active | boolean |
| pbx_extension | string or null^\d{1,8}$ |
{- "active": true,
- "pbx_extension": "string"
}{ }{- "campaigns": [
- {
- "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": {
- "Boké": 3,
- "Conakry": 3,
- "Kindia": 3,
- "Kankan": 3,
- "Mamou": 3,
- "Labé": 2,
- "Faranah": 2,
- "Nzérékoré": 2
}, - "draw_participant_scope": "ALL",
- "created_by": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]
}| 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 — | |
| draw_participant_scope | string Default: "ALL" Enum: "ALL" "SINCE_LAST_DRAW" Which farmers a draw pools (Phase 4-F). |
| created_by | string or null |
| created_at | string <date-time> |
| updated_at | string <date-time> |
{- "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": {
- "Boké": 3,
- "Conakry": 3,
- "Kindia": 3,
- "Kankan": 3,
- "Mamou": 3,
- "Labé": 2,
- "Faranah": 2,
- "Nzérékoré": 2
}, - "draw_participant_scope": "ALL",
- "created_by": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "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": {
- "Boké": 3,
- "Conakry": 3,
- "Kindia": 3,
- "Kankan": 3,
- "Mamou": 3,
- "Labé": 2,
- "Faranah": 2,
- "Nzérékoré": 2
}, - "draw_participant_scope": "ALL",
- "created_by": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "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": {
- "Boké": 3,
- "Conakry": 3,
- "Kindia": 3,
- "Kankan": 3,
- "Mamou": 3,
- "Labé": 2,
- "Faranah": 2,
- "Nzérékoré": 2
}, - "draw_participant_scope": "ALL",
- "created_by": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}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.
| cid required | string <uuid> |
| property name* additional property | any |
{ }{- "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": {
- "Boké": 3,
- "Conakry": 3,
- "Kindia": 3,
- "Kankan": 3,
- "Mamou": 3,
- "Labé": 2,
- "Faranah": 2,
- "Nzérékoré": 2
}, - "draw_participant_scope": "ALL",
- "created_by": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| cid required | string <uuid> |
{- "campaign_code": "string",
- "rules": [
- {
- "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"
}
]
}| cid required | string <uuid> |
| rule_type required | string |
object | |
| priority | integer |
| notes | string |
{- "rule_type": "string",
- "params": { },
- "priority": 0,
- "notes": "string"
}{- "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"
}| cid required | string <uuid> |
| rid required | string <uuid> |
| property name* additional property | any |
{ }{- "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"
}Returns 409 with error: scenarios_not_passed or tc_not_updated
if the 4-step workflow has not been satisfied.
| cid required | string <uuid> |
| rid required | string <uuid> |
{- "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"
}| cid required | string <uuid> |
| rid required | string <uuid> |
{- "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"
}| rid required | string <uuid> |
{- "rule_type": "string",
- "scenarios": [
- {
- "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": { }
}
]
}| rid required | string <uuid> |
| 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 |
{- "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": { }
}{- "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": { }
}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.
| campaign_code required | string |
| sender_phone required | string |
| message_text required | string |
| simulated_now | string <date-time> |
object |
{- "campaign_code": "roko-super-lot-2026",
- "sender_phone": "+224612000001",
- "message_text": "ROKO U12345678",
- "simulated_now": "2019-08-24T14:15:22Z",
- "fixtures": { }
}{- "ok": true,
- "campaign": {
- "id": 0,
- "code": "string",
- "name": "string"
}, - "matchedRule": {
- "id": 0,
- "ruleType": "string",
- "priority": 0
}, - "replyTemplateKey": "string",
- "replyText": "string",
- "silent": true,
- "trace": [
- { }
], - "sideEffects": [
- { }
], - "dryRun": true
}| limit | integer <= 500 Default: 200 |
| offset | integer Default: 0 |
| campaign_id | integer |
{- "entries": [
- {
- "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"
}
], - "total": 0,
- "limit": 0,
- "offset": 0
}| phone_number required | string |
| reason required | string |
| campaign_id | integer or null |
object |
{- "phone_number": "string",
- "reason": "string",
- "campaign_id": 0,
- "metadata": { }
}{- "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"
}| cid required | string <uuid> |
{- "campaign_code": "string",
- "versions": [
- {
- "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"
}
]
}| cid required | string <uuid> |
| language required | string Enum: "EN" "FR" |
| body_md required | string |
{- "language": "EN",
- "body_md": "string"
}{- "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"
}| cid required | string <uuid> |
| lang required | string Enum: "EN" "FR" |
| version required | integer >= 1 |
{- "version": 1
}