Products · Developer guides
Example Flows
Complete example flows for school fees, church giving, prepaid electricity, event tickets and airtime.
Complete example flows that combine context stores, side-effect pages and payments. The businesses, numbers, payment target refs and URLs are made up. Replace PAYMENT_TARGET_REF_PLACEHOLDER with a Payment Code or UVAN from your Monime space, and the https://example.com/... URLs with your own endpoints.
Flows that call an exchange endpoint need a public key before you can publish them. See Exchange & Dynamic API.
1. School Fees
Salone School Fees lets parents pay a child's school fees. On the first visit the parent enters a student ID, and the flow saves the student. On later visits, the first menu item pays for the saved student.
Uses: recurring context, options.skip, menu items with hide, a side-effect page that saves server data, collect_payment reading rc.*.
Your validate-student endpoint replies with navigate to save_student, with student_name and school_name in pageData. If the ID is unknown, it replies with stop and a message.
{
"version": "1.0",
"pages": [
{
"id": "start_menu",
"component": {
"type": "menu",
"label": "Salone School Fees.\nParent: $(rc.parent_name ?? 'Guest')",
"items": [
{ "label": "Pay for $(rc.student_name)", "value": "saved", "hide": "$(strings.is_blank(rc.student_id))" },
{ "label": "Add or change student", "value": "new" },
{ "label": "Exit", "value": "exit" }
]
}
},
{
"id": "collect_parent_name",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Enter your full name",
"min_length": 3,
"max_length": 50
},
"options": {
"skip": "$(page.start_menu.value != 'new' || !strings.is_blank(rc.parent_name))"
},
"on_done": {
"action": "next",
"recurring_context": {
"mutations": {
"parent_name": "$(strings.title(page.collect_parent_name.value))"
}
}
}
},
{
"id": "collect_student_id",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Enter the 10-digit student ID",
"pattern": "^[0-9]{10}$",
"pattern_mismatch_message": "Student IDs have 10 digits. Try again."
},
"options": {
"skip": "$(page.start_menu.value != 'new')"
},
"on_done": {
"action": "exchange",
"url": "https://example.com/ussd/validate-student"
}
},
{
"id": "save_student",
"component": { "type": "side_effect" },
"options": {
"skip": "$(page.start_menu.value != 'new')"
},
"on_done": {
"action": "next",
"recurring_context": {
"mutations": {
"student_id": "$(page.collect_student_id.value)",
"student_name": "$(data.student_name)",
"school_name": "$(data.school_name)"
}
}
}
},
{
"id": "pick_term",
"component": {
"type": "menu",
"label": "$(rc.student_name) at $(rc.school_name).\nSelect term:",
"items": [
{ "label": "Term 1 (SLE 1,500)", "value": "1500" },
{ "label": "Term 2 (SLE 1,500)", "value": "1500" },
{ "label": "Term 3 (SLE 1,500)", "value": "1500" },
{ "label": "Full year (SLE 4,200)", "value": "4200" }
]
},
"options": {
"skip": "$(page.start_menu.value == 'exit')"
},
"on_done": {
"action": "next",
"session_context": {
"mutations": { "chosen_term": "$(page.pick_term.label)" }
}
}
},
{
"id": "charge_fees",
"component": {
"type": "template",
"template_name": "collect_payment",
"label": "Enter your $(session.network_provider_name) PIN to pay {{amount}} for $(rc.student_name), $(sc.chosen_term).",
"template_data": {
"payment_target_ref": "PAYMENT_TARGET_REF_PLACEHOLDER",
"amount": "$(page.pick_term.value)",
"reference": "$(rc.student_id)",
"metadata": {
"student_id": "$(rc.student_id)",
"term": "$(sc.chosen_term)"
}
}
},
"options": {
"skip": "$(page.start_menu.value == 'exit')"
}
},
{
"id": "fees_done",
"component": {
"type": "display",
"label": "Thank you $(strings.split_index(rc.parent_name, ' ', 0)).\n$(sc.chosen_term) paid for $(rc.student_name).\nRef: $(page.charge_fees.value)"
},
"options": {
"skip": "$(page.start_menu.value == 'exit')"
}
},
{
"id": "bye",
"component": {
"type": "display",
"label": "Thank you for using Salone School Fees."
}
}
]
}What to notice:
- The "Pay for …" item is hidden until
rc.student_idis saved. Returning parents see it first. - The parent's name is asked once. After that,
collect_parent_nameskips itself. - The student is saved in
rc.*because it lasts across visits. The chosen term is only needed for this payment, so it goes insc.*. - When the parent picks "Exit", every page up to
byeskips itself.
2. Weekly Giving
Kissy Community Church lets members give each week. The flow remembers the member's name, last amount and type of gift, so "Repeat last" takes one key press.
Uses: expressions in labels, a side-effect page that stages the amount, options.terminal with a thank-you message, datetime.extract.
{
"version": "1.0",
"pages": [
{
"id": "start_menu",
"component": {
"type": "menu",
"label": "Kissy Community Church giving.\n$(rc.display_name != null ? 'Welcome back ' + strings.split_index(rc.display_name, ' ', 0) + '.' : 'Welcome.')",
"items": [
{ "label": "Repeat last (SLE $(rc.last_amount))", "value": "repeat", "hide": "$(strings.is_blank(rc.last_amount))" },
{ "label": "Tithe", "value": "tithe" },
{ "label": "Offering", "value": "offering" },
{ "label": "Special gift", "value": "special" }
]
}
},
{
"id": "collect_display_name",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Enter your full name",
"min_length": 3,
"max_length": 40
},
"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": "collect_amount",
"component": {
"type": "prompt",
"data_type": "integer",
"label": "Enter amount in SLE",
"min_value": 10,
"max_value": 100000
},
"options": {
"skip": "$(page.start_menu.value == 'repeat')"
}
},
{
"id": "stage_gift",
"component": { "type": "side_effect" },
"on_done": {
"action": "next",
"session_context": {
"mutations": {
"gift_amount": "$(page.start_menu.value == 'repeat' ? rc.last_amount : page.collect_amount.value)",
"gift_kind": "$(page.start_menu.value == 'repeat' ? rc.last_kind : page.start_menu.value)"
}
}
}
},
{
"id": "charge_gift",
"component": {
"type": "template",
"template_name": "collect_payment",
"label": "Enter your $(session.network_provider_name) PIN to give {{amount}} to Kissy Community Church.",
"template_data": {
"payment_target_ref": "PAYMENT_TARGET_REF_PLACEHOLDER",
"amount": "$(sc.gift_amount)",
"reference": "$(rc.display_name)",
"metadata": {
"kind": "$(sc.gift_kind)",
"weekday": "$(datetime.extract(null, 'E'))"
}
}
},
"options": {
"terminal": true,
"terminal_message": "Thank you $(strings.split_index(rc.display_name, ' ', 0)). God bless you."
},
"on_done": {
"action": "next",
"recurring_context": {
"mutations": {
"last_amount": "$(sc.gift_amount)",
"last_kind": "$(sc.gift_kind)"
}
}
}
}
]
}What to notice:
collect_payment.amounttakes only a single reference, not an expression. Sostage_giftworks out the amount with an expression, saves it insc.gift_amount, and the payment page reads that key.- "Repeat last" is hidden until the member has given once. A member who picks it goes from the menu to the PIN prompt in one step.
terminal_messageshows the thank-you and ends the session, so no display page is needed.metadata.weekdayis the day of the week, 1 (Monday) to 7 (Sunday), so you can group gifts by day in your reports.
3. Prepaid Electricity
Lumley Power sells prepaid electricity. The flow checks the meter number with your server, shows the customer's name before charging, and remembers the last meter used.
Uses: an exchange from a side-effect page, dataExport, writing to both sc.* and rc.*, a boolean prompt with exit_message.
Your validate-meter endpoint receives the meter in exportedData.meter. It replies with navigate to save_meter, with customer_name and tariff in pageData. If the meter is unknown, it replies with stop and a message.
{
"version": "1.0",
"pages": [
{
"id": "start_menu",
"component": {
"type": "menu",
"label": "Lumley Power prepaid top-up",
"items": [
{ "label": "Last meter: $(rc.last_meter)", "value": "last", "hide": "$(strings.is_blank(rc.last_meter))" },
{ "label": "Enter a meter number", "value": "new" }
]
}
},
{
"id": "collect_meter",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Enter the 11-digit meter number",
"pattern": "^[0-9]{11}$",
"pattern_mismatch_message": "Meter numbers have 11 digits. Try again."
},
"options": {
"skip": "$(page.start_menu.value == 'last')"
}
},
{
"id": "check_meter",
"component": { "type": "side_effect" },
"on_done": {
"action": "exchange",
"url": "https://example.com/ussd/validate-meter",
"session_context": {
"mutations": {
"meter_number": "$(page.start_menu.value == 'last' ? rc.last_meter : page.collect_meter.value)"
}
},
"dataExport": {
"meter": "$(sc.meter_number)"
}
}
},
{
"id": "save_meter",
"component": { "type": "side_effect" },
"on_done": {
"action": "next",
"session_context": {
"mutations": {
"customer_name": "$(data.customer_name)",
"tariff": "$(data.tariff)"
}
},
"recurring_context": {
"mutations": {
"last_meter": "$(sc.meter_number)"
}
}
}
},
{
"id": "confirm_meter",
"component": {
"type": "prompt",
"data_type": "boolean",
"label": "Meter: $(sc.meter_number)\nCustomer: $(sc.customer_name)\nContinue?",
"exit_message": "Cancelled. Dial again to use a different meter."
}
},
{
"id": "collect_amount",
"component": {
"type": "prompt",
"data_type": "integer",
"label": "Enter amount in SLE (10 to 5,000)",
"min_value": 10,
"max_value": 5000
}
},
{
"id": "charge_topup",
"component": {
"type": "template",
"template_name": "collect_payment",
"label": "Enter your $(session.network_provider_name) PIN to pay {{amount}} for meter $(sc.meter_number).",
"template_data": {
"payment_target_ref": "PAYMENT_TARGET_REF_PLACEHOLDER",
"amount": "$(page.collect_amount.value)",
"reference": "$(sc.meter_number)",
"metadata": {
"meter": "$(sc.meter_number)",
"tariff": "$(sc.tariff)"
}
}
}
},
{
"id": "topup_done",
"component": {
"type": "display",
"label": "Paid SLE $(page.collect_amount.value) for meter $(sc.meter_number).\nRef: $(page.charge_topup.value)"
}
}
]
}What to notice:
- Both menu choices lead to
check_meter. Its mutation picks the saved meter or the new one, and it runs before the exchange, sodataExportcan send it. $(data.*)only holds what your server sent for the current page. That's whysave_metercopiescustomer_nameandtariffintosc.*for the pages after it.save_meterwrites to both stores:sc.*for this session andrc.last_meterfor the next visit.- The boolean prompt's
exit_messageends the session when the customer answers "No".
4. Event Tickets
Freetown Live sells 1 to 10 tickets to a concert, at one of three prices. The multiplier charges the ticket price times the number of tickets.
Uses: multiplier from a prompt, {{multiplier}} in the receipt name, a linear flow.
{
"version": "1.0",
"pages": [
{
"id": "welcome_get_name",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Welcome to Freetown Live.\nEnter your name to continue.",
"min_length": 2,
"max_length": 40
}
},
{
"id": "select_ticket_tier",
"component": {
"type": "menu",
"label": "Select ticket type",
"items": [
{ "label": "Regular - SLE 200", "value": "200" },
{ "label": "VIP - SLE 1,000", "value": "1000" },
{ "label": "VVIP - SLE 1,500", "value": "1500" }
]
}
},
{
"id": "get_ticket_count",
"component": {
"type": "prompt",
"data_type": "integer",
"label": "How many $(page.select_ticket_tier.label) tickets? (1 to 10)",
"min_value": 1,
"max_value": 10
}
},
{
"id": "charge_tickets",
"component": {
"type": "template",
"template_name": "collect_payment",
"label": "Enter your $(session.network_provider_name) PIN to pay {{amount}} for $(page.get_ticket_count.value) x $(page.select_ticket_tier.label).",
"template_data": {
"payment_target_ref": "PAYMENT_TARGET_REF_PLACEHOLDER",
"amount": "$(page.select_ticket_tier.value)",
"multiplier": "$(page.get_ticket_count.value)",
"reference": "$(page.welcome_get_name.value)",
"receipt_name": "{{multiplier}} x Freetown Live ticket",
"metadata": {
"tier": "$(page.select_ticket_tier.value)",
"quantity": "$(page.get_ticket_count.value)"
}
}
}
},
{
"id": "acknowledge",
"component": {
"type": "display",
"label": "Thank you $(strings.split_index(page.welcome_get_name.value, ' ', 0)).\nShow this reference at the gate:\n$(page.charge_tickets.value)"
}
}
]
}What to notice:
- Monime charges
amounttimesmultiplier, and{{amount}}in the PIN prompt is that total. multiplierfollows the same rule asamount: a number or a single reference.- The PIN prompt repeats the count and ticket type, for example "3 x VIP - SLE 1,000", so the buyer can check before entering their PIN.
- The flow uses no
rc.*orsc.*. Each visit is a single purchase.
5. Airtime Top-up
Makeni Airtime sells airtime for the subscriber's own number or a friend's. The menu offers the last amount as a quick option.
Uses: rc.last_amount, menu items with hide, a pattern check, a side-effect page that stages values for collect_payment.
{
"version": "1.0",
"pages": [
{
"id": "start_menu",
"component": {
"type": "menu",
"label": "Makeni Airtime",
"items": [
{ "label": "My number, SLE $(rc.last_amount) again", "value": "self_last", "hide": "$(strings.is_blank(rc.last_amount))" },
{ "label": "My number, new amount", "value": "self_new" },
{ "label": "A friend's number", "value": "other" }
]
}
},
{
"id": "get_target_number",
"component": {
"type": "prompt",
"data_type": "text",
"label": "Enter your friend's number, for example 076123456",
"pattern": "^0[0-9]{8}$",
"pattern_mismatch_message": "Enter 9 digits starting with 0. Try again."
},
"options": {
"skip": "$(page.start_menu.value != 'other')"
}
},
{
"id": "get_amount",
"component": {
"type": "prompt",
"data_type": "integer",
"label": "Enter amount (SLE 5 to 500)",
"min_value": 5,
"max_value": 500
},
"options": {
"skip": "$(page.start_menu.value == 'self_last')"
}
},
{
"id": "stage_charge",
"component": { "type": "side_effect" },
"on_done": {
"action": "next",
"session_context": {
"mutations": {
"charge_amount": "$(page.start_menu.value == 'self_last' ? rc.last_amount : page.get_amount.value)",
"target": "$(page.start_menu.value == 'other' ? page.get_target_number.value : 'self')",
"target_label": "$(page.start_menu.value == 'other' ? page.get_target_number.value : 'your number')"
}
}
}
},
{
"id": "charge_airtime",
"component": {
"type": "template",
"template_name": "collect_payment",
"label": "Enter your $(session.network_provider_name) PIN to buy {{amount}} of airtime for $(sc.target_label).",
"template_data": {
"payment_target_ref": "PAYMENT_TARGET_REF_PLACEHOLDER",
"amount": "$(sc.charge_amount)",
"reference": "$(sc.target)",
"metadata": {
"target": "$(sc.target)"
}
}
},
"on_done": {
"action": "next",
"recurring_context": {
"mutations": {
"last_amount": "$(sc.charge_amount)"
}
}
}
},
{
"id": "topup_done",
"component": {
"type": "display",
"label": "Done. SLE $(sc.charge_amount) of airtime for $(sc.target_label) is on its way.\nRef: $(page.charge_airtime.value)"
}
}
]
}What to notice:
stage_chargeis the usual way to compute acollect_paymentfield. It works out the amount and target with expressions, saves them insc.*, and the payment page reads plain references.- For the subscriber's own number, the flow saves
selfinstead of the number. The payment already records who paid. If you save the subscriber's own number, it is masked unless they give permission with get_subscriber_msisdn. - "SLE … again" is hidden until the first top-up. After each payment,
rc.last_amountis updated.
Patterns at a Glance
| Pattern | How | Example |
|---|---|---|
| Skip questions for returning subscribers | options.skip on rc.* | "skip": "$(!strings.is_blank(rc.display_name))" |
| Show menu items only when they apply | Item hide | "hide": "$(strings.is_blank(rc.last_amount))" |
| Default for a missing value | ?? inside an expression | $(rc.parent_name ?? 'Guest') |
| End with a message after a page | options.terminal and terminal_message | Weekly giving |
| Stop when the subscriber says no | Boolean prompt with exit_message | Prepaid electricity |
| Keep server data for later pages | Side-effect page copying data.* into sc.* | save_meter |
| Compute a payment amount | Side-effect page writing sc.* before collect_payment | stage_gift, stage_charge |
| Charge for several units | multiplier from a prompt | Event tickets |
Related
- Flow Definition: the page schema
- Context Stores: how
rc.*andsc.*work - Expressions: operators and functions
- Side Effect: pages that run logic without a screen
- collect_payment: the payment template