# Example Flows (/docs/products/ussd-flows/developers/examples)

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](/docs/products/ussd-flows/developers/exchange-api/overview).

## 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](/docs/products/ussd-flows/developers/templates/get-subscriber-msisdn).
- "SLE … again" is hidden until the first top-up. After each payment, `rc.last_amount` is 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](/docs/products/ussd-flows/developers/flow-definition): the page schema
- [Context Stores](/docs/products/ussd-flows/developers/context-stores): how `rc.*` and `sc.*` work
- [Expressions](/docs/products/ussd-flows/developers/expressions): operators and functions
- [Side Effect](/docs/products/ussd-flows/developers/page-types/side-effect): pages that run logic without a screen
- [collect\_payment](/docs/products/ussd-flows/developers/templates/collect-payment): the payment template
