Skip to content

Tutorial: PIN login

Two-factor login in a USSD menu: look up the account by the dialling number, check the PIN with your API, and branch on each reply.

What you will build

A balance check behind a login. Your API is called twice: once to find the account for the phone that is dialling, and once to check the PIN the caller types. Each reply decides where the caller goes next.

Welcome back, Ama.
Enter your 4-digit PIN:
A registered caller
Your balance is GHS 1,250.40.
After the right PIN
StepTypeWhat it does
WelcomeEntry PointOffers Check balance or Exit.
Find accountSend DataLooks up the account by {{MobileNumber}}.
Registered?Smart RouterSends unknown numbers to a sign-up message.
Ask for PINAsk a QuestionSaves the answer as pin.
Check PINSend DataSends the number and PIN; gets the balance back.
PIN right?Smart RouterShows the balance, or a wrong-PIN message.
Four closing screensEnd SessionBalance, not registered, wrong PIN, try again later.

Why this is two factors

{{MobileNumber}} is not typed by the caller. The mobile network supplies it with every request, so it proves the caller is holding that SIM. The PIN proves they know the secret. A stolen PIN is no use from another phone, and a borrowed phone is no use without the PIN.

Watch out: Never ask the caller to type their own number to identify themselves. Anyone can type anyone's number. Use {{MobileNumber}}, which a step in your app cannot overwrite.

What your API needs

1. Find the account

GET https://api.example.com/accounts/233241234567
{ "registered": true, "firstName": "Ama" }

For a number with no account, answer { "registered": false }, not an error.

2. Check the PIN

POST https://api.example.com/accounts/login
{ "phone": "233241234567", "pin": "4821" }
Reply
{ "verified": true, "balance": "1,250.40" }
{ "verified": false, "attemptsLeft": 2 }
Tip: Count wrong attempts in your API and lock the account there after a few. Your API is the only place that sees every attempt, across every session.

Build it

  1. Set up the welcome screen

    On the Entry Point, set the text to Sika Savings and the options to Check balance and Exit.
  2. Find the account

    Add Send Data. Choose GET and put the caller's number in the address:
    Address
    https://api.example.com/accounts/{{MobileNumber}}
    Under Save From Response, add $.registered → registered and $.firstName → firstName.
  3. Branch on whether they are registered

    Add a Smart Router with one branch: registered == true.
  4. Ask for the PIN

    Add Ask a Question. Text:
    What the phone shows
    Welcome back, {{firstName}}.
    Enter your 4-digit PIN:
    In the Answer section set Save the answer as to pin and Answer type to PIN / Secret, so only digits are accepted.
  5. Check the PIN

    Add a second Send Data. Choose POST, enter the login address, and set the body:
    Request body
    {
      "phone": "{{MobileNumber}}",
      "pin": "{{pin}}"
    }
    Save $.verified → verified and $.balance → balance.
  6. Branch on the result

    Add a second Smart Router with one branch: verified == true.
  7. Add the closing screens

    Add four End Session screens:
    • Your balance is GHS {{balance}}.
    • This number has no Sika Savings account. Visit a branch to open one.
    • That PIN is not right. Dial again to retry.
    • We cannot reach your account right now. Please try again later.
    Add one more with Goodbye. for Exit.

Join the steps

FromTo
Welcome, option Check balanceFind account
Welcome, option ExitGoodbye.
Find account, bottom dotRegistered? router
Find account, FAILWe cannot reach your account right now.
Registered? first branchAsk for PIN
Registered? ELSEThis number has no Sika Savings account.
Ask for PINCheck PIN
Check PIN, bottom dotPIN right? router
Check PIN, FAILWe cannot reach your account right now.
PIN right? first branchYour balance is…
PIN right? ELSEThat PIN is not right.
Good to know: A value your API did not send is treated as not matching. If the lookup returns no registered field at all, the caller leaves by ELSE, which here is the safe direction: they are not let in.

Test it

  1. Open the phone preview

    Press Test in the top bar. In Dial from, type the number the test should call from, such as 0241234567. Your app receives it as {{MobileNumber}}.
  2. A registered number, right PIN

    Set Dial from to a number your API knows. Reply 1, then the PIN. You should see the balance.
  3. Wrong PIN

    Dial again and type a wrong PIN. You should see the wrong-PIN message, with no balance.
  4. An unregistered number

    Change Dial from to a number with no account. You should never be asked for a PIN.
  5. A PIN that is not digits

    Reply abcd at the PIN question. It is refused before your API is called.
Good to know: In the preview, Send Data calls your API from your browser, so the API must be on https:// and allow requests from the dashboard (CORS). On a real phone the call comes from Asterisks' servers and CORS does not apply. Testing against your laptop? An ngrok address works: the preview adds the header ngrok asks for.

Make it yours

  • Another try in the same session: connect the wrong-PIN branch back to the PIN question, and have your API answer locked when attempts run out so a third branch can end the session.
  • A menu after login: replace the balance screen with a Choice Menu (balance, mini statement, change PIN). Each option can call your API with {{MobileNumber}}.
  • A one-time code by SMS: have your login API send the code, then add another question and Send Data step to check it. The caller may not be able to open an SMS while the USSD session is on screen, so tell them they may need to dial again.
  • Login as a reusable piece: publish this app and run it from others with a Sub-flow step, passing verified back out.