NewDash Dots are hereMini apps in Dash, with payments built inLearn more

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.

JSON
{
  "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_id is saved. Returning parents see it first.
  • The parent's name is asked once. After that, collect_parent_name skips itself.
  • The student is saved in rc.* because it lasts across visits. The chosen term is only needed for this payment, so it goes in sc.*.
  • When the parent picks "Exit", every page up to bye skips 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.

JSON
{
  "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.amount takes only a single reference, not an expression. So stage_gift works out the amount with an expression, saves it in sc.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_message shows the thank-you and ends the session, so no display page is needed.
  • metadata.weekday is 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.

JSON
{
  "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, so dataExport can send it.
  • $(data.*) only holds what your server sent for the current page. That's why save_meter copies customer_name and tariff into sc.* for the pages after it.
  • save_meter writes to both stores: sc.* for this session and rc.last_meter for the next visit.
  • The boolean prompt's exit_message ends 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.

JSON
{
  "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 amount times multiplier, and {{amount}} in the PIN prompt is that total.
  • multiplier follows the same rule as amount: 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.* or sc.*. 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.

JSON
{
  "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_charge is the usual way to compute a collect_payment field. It works out the amount and target with expressions, saves them in sc.*, and the payment page reads plain references.
  • For the subscriber's own number, the flow saves self instead 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_amount is updated.

Patterns at a Glance

PatternHowExample
Skip questions for returning subscribersoptions.skip on rc.*"skip": "$(!strings.is_blank(rc.display_name))"
Show menu items only when they applyItem 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 pageoptions.terminal and terminal_messageWeekly giving
Stop when the subscriber says noBoolean prompt with exit_messagePrepaid electricity
Keep server data for later pagesSide-effect page copying data.* into sc.*save_meter
Compute a payment amountSide-effect page writing sc.* before collect_paymentstage_gift, stage_charge
Charge for several unitsmultiplier from a promptEvent tickets