Products · Developer guides
Flow Definition
The JSON format of a USSD flow: pages, component types, limits, page options and what happens when a page is done.
A flow is a JSON document with a list of pages. The subscriber moves through the pages in order unless a page sends them somewhere else.
Top-Level Structure
{
"version": "1.0",
"pages": [
{
"id": "welcome",
"component": { "type": "display", "label": "Welcome to Kenema Water." }
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
version | string | Yes | Flow format version. Use "1.0". |
pages | array | Yes | The pages, in order. At least one. |
Any other top-level field fails validation.
Pages
{
"id": "enter_amount",
"component": { "type": "prompt", "data_type": "integer", "label": "Enter amount in SLE" },
"options": { "skip": false },
"on_done": { "action": "next" }
}| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The page's ID. See the rules below. |
component | object | Yes | What the page shows and how the subscriber answers. |
options | object | No | Skip the page, or end the flow after it. See Page Options. |
on_done | object | No | What happens when the subscriber finishes the page. See Page Completion. |
Page ID Rules
- Start with a lowercase letter.
- Use only lowercase letters, digits and underscores, for example
enter_amount. - Make each ID unique within the flow.
Not every rule is checked when you save. An ID that breaks them can't be used in on_done.next_page, and an ID with a hyphen can't be read with $(page.ID.value). For naming style, see Authoring Conventions.
Component Types
component.type sets the kind of page. There are six:
| Type | What it does | Subscriber input |
|---|---|---|
display | Shows a message and ends the flow | None |
prompt | Asks for text, a number, or yes or no | Required |
menu | Shows numbered options | Required |
dynamic | Asks your server what to show next | Depends on your server |
template | Runs a built-in operation, such as taking a payment | Depends on the template |
side_effect | Shows nothing; only runs its on_done and moves on | None |
Limits
| What | Limit | Over the limit |
|---|---|---|
| Page label, as the subscriber sees it (display, prompt, menu, template) | 130 characters, after every $(...) is filled in | Cut off at 130 |
| Menu item label, as the subscriber sees it | 64 characters | Cut off at 64 |
Label as written in the JSON, including $(...) | 1,000 characters | Fails validation |
| Items in a fixed menu list | 1 to 16 | Fails validation |
| Pages in a flow | At least 1 | Fails validation |
| Context key in a mutation | 1 to 33 characters: letters, digits and underscores, not starting with a digit | Fails validation |
Text written as-is in a mutation or dataExport | 100 characters | Fails validation |
Labels and menu items are cut off, not rejected. Check the longest version of each label, with real names and amounts filled in. For the limits on stored values, such as the 16-key limit on recurring context, see Context Stores.
Page Options
options decides whether a page runs and whether the flow ends after it.
{
"options": {
"skip": "$(rc.has_signed_up)",
"terminal": false,
"terminal_message": "Thanks. See you next week."
}
}| Field | Type | Description |
|---|---|---|
skip | boolean or $(...) | When true, the page is passed over. Its on_done does not run, and the flow moves to the next page in the list. |
terminal | boolean or $(...) | When true, the flow ends after this page. |
terminal_message | string | The last message the subscriber sees when terminal ends the flow. Can contain $(...). If empty, the subscriber sees "Flow session cancelled." |
A $(...) in skip or terminal must give exactly true to take effect. Other text, such as a name, counts as false. To test whether a value is set, use a function: "$(!strings.is_blank(rc.display_name))". See Expressions.
Skip
{
"id": "collect_display_name",
"component": { "type": "prompt", "data_type": "text", "label": "Enter your name" },
"options": { "skip": "$(!strings.is_blank(rc.display_name))" }
}A skipped page always moves to the next page in the list, even if its on_done has a next_page. If a skipped page is the last page, the flow stops with an error.
Terminal
{
"id": "confirm_signup",
"component": { "type": "prompt", "data_type": "boolean", "label": "Join Bo Fresh Rewards for free?" },
"options": {
"terminal": "$(page.confirm_signup.value == 'true')",
"terminal_message": "You are in. Dial again to see your rewards."
},
"on_done": {
"action": "next",
"recurring_context": { "mutations": { "has_signed_up": "$(page.confirm_signup.value)" } }
}
}terminal is checked after on_done runs, so the page's mutations are saved before the flow ends. terminal has no effect on a display page, because a display page always ends the flow.
Page Completion (on_done)
on_done runs when the subscriber finishes a page. Without it, the flow moves to the next page in the list.
{
"on_done": {
"action": "next",
"next_page": "confirm_payment",
"session_context": { "mutations": { "chosen_term": "$(page.select_term.label)" } },
"recurring_context": { "mutations": { "has_paid_before": true } }
}
}| Field | Type | Description |
|---|---|---|
action | "next" or "exchange" | "next" (the default) moves on in the flow. "exchange" calls your server, which picks the next page. |
next_page | string | Only with "next". The page to go to instead of the next one in the list: a page ID, or a $(...) that gives a page ID. |
url | string | Required with "exchange". Your endpoint, as a full fixed URL. $(...) is not filled in here. |
dataExport | object | Only with "exchange". Extra values to send to your endpoint. See Sending Extra Values. |
session_context.mutations | object | Values to save for the rest of this session, read later as $(sc.KEY). |
recurring_context.mutations | object | Values to save for this subscriber across sessions, read later as $(rc.KEY). |
Mutations Run Before Navigation
When a page is done, Monime saves its session_context mutations, then its recurring_context mutations, and only then moves to the next page or calls your server. The next page and your server can read the new values.
A display page's mutations run when the page is shown. The rules for each store are in Context Stores.
next
Moves to the next page in the list. This is what happens when on_done is left out.
next with next_page
Jumps to another page without calling your server:
{
"id": "enter_amount",
"component": { "type": "prompt", "data_type": "integer", "label": "Enter amount in SLE" },
"on_done": { "action": "next", "next_page": "confirm_payment" }
}next_page can also be a $(...) that gives a page ID. Use it to branch on an answer:
{
"on_done": {
"action": "next",
"next_page": "$(page.start_menu.value == 'exit' ? 'say_goodbye' : 'enter_account')"
}
}A written page ID must follow the page ID rules. An empty next_page fails validation. If the ID doesn't match any page, the flow stops with an error when it gets there.
exchange
Calls your server with the session details and every answer so far. Your server replies with the next page to show (and optional data for it), or with a message that ends the flow. See the Exchange API.
{
"id": "enter_meter_number",
"component": { "type": "prompt", "data_type": "text", "label": "Enter your meter number" },
"on_done": {
"action": "exchange",
"url": "https://api.example.com/ussd/meter"
}
}next_page can't be used with "exchange".
Sending Extra Values (dataExport)
Every exchange request already carries the answers from earlier pages in flowData. Use dataExport when your server also needs a stored value or a computed one. Monime sends it as exportedData in the request.
{
"on_done": {
"action": "exchange",
"url": "https://api.example.com/ussd/meter",
"dataExport": {
"meter_number": "$(sc.meter_number)",
"district": "Bo",
"is_new_customer": "$(strings.is_blank(rc.last_meter))",
"retry_count": 1
}
}
}- Keys are your own names.
- Each value is
null,trueorfalse, a number, text of up to 100 characters, or a$(...). - Objects and lists are not allowed.
- A
$(...)value is always sent as text. Above,is_new_customerarrives as"true"or"false".
If a value is the subscriber's own phone number, your server receives it masked. The exception is a number that came from a get_subscriber_msisdn page, either directly or through a context key saved straight from that page. See Subscriber Identification.
Complete Example
A rewards flow that remembers returning subscribers. New subscribers give their name and pay a joining fee. Members choose a bundle.
{
"version": "1.0",
"pages": [
{
"id": "collect_display_name",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Welcome to Bo Fresh Rewards.\nEnter your name",
"min_length": 2,
"max_length": 40,
"max_invalid_input_attempts": 3
},
"options": {
"skip": "$(!strings.is_blank(rc.display_name))"
},
"on_done": {
"action": "next",
"recurring_context": {
"mutations": { "display_name": "$(strings.title(page.collect_display_name.value))" }
}
}
},
{
"id": "choose_bundle",
"component": {
"type": "menu",
"label": "Hi $(strings.split_index(rc.display_name, ' ', 0)), pick an option",
"items": [
{ "label": "Join (SLE 100)", "value": "100", "hide": "$(rc.is_member == 'true')" },
{ "label": "Small bundle (SLE 20)", "value": "20", "hide": "$(rc.is_member != 'true')" },
{ "label": "Large bundle (SLE 50)", "value": "50", "hide": "$(rc.is_member != 'true')" }
]
}
},
{
"id": "charge_bundle",
"component": {
"type": "template",
"template_name": "collect_payment",
"label": "Enter your $(session.network_provider_name) PIN to pay for $(page.choose_bundle.label).",
"template_data": {
"amount": "$(page.choose_bundle.value)",
"payment_target_ref": "YOUR_PAYMENT_CODE",
"receipt_name": "Bo Fresh Rewards",
"reference": "$(rc.display_name)"
}
},
"options": {
"terminal": "$(page.choose_bundle.value == '100')",
"terminal_message": "Welcome to Bo Fresh Rewards, $(strings.split_index(rc.display_name, ' ', 0))."
},
"on_done": {
"action": "next",
"recurring_context": {
"mutations": { "is_member": "$(page.choose_bundle.value == '100' || rc.is_member == 'true')" }
}
}
},
{
"id": "bundle_done",
"component": {
"type": "display",
"label": "Thanks $(strings.split_index(rc.display_name, ' ', 0)). Your $(page.choose_bundle.label) is active."
}
}
]
}Replace YOUR_PAYMENT_CODE with a Payment Code or UVAN from your Monime Space. The payment page itself is covered in collect_payment.
- collect_display_name asks for a name, unless one is already saved. It saves the name in title case as
rc.display_name. - choose_bundle shows only "Join" to new subscribers, and only the bundles to members.
- charge_bundle takes the payment and saves
rc.is_member. A joining payment ends the flow with a welcome message. - bundle_done confirms a bundle and ends the flow.
On the next dial, a member skips the name prompt and sees only the bundles.
For more complete flows, see Example Flows. For naming and flow shapes, see Authoring Conventions.