What you'll build
In the final part of this series you'll add SMS confirmation messages so users receive a receipt on their phone after every successful transaction. Then you'll deploy the service to Railway with environment variables set correctly, and switch to production Africa's Talking credentials.
By the end of this part:
- Every completed payment sends an SMS receipt to the user's phone
- Airtime purchases confirm via SMS with the credited amount
- The service runs on Railway with a stable public URL
- The AT USSD channel points at the live URL, not ngrok
Build the SMS service
Create src/services/sms.ts for the GH Utilities USSD service.
Reuse the Africa's Talking client initialised in airtime.ts (export it from airtime.ts and import here, or initialise separately using the same env vars).
Export sendSms(to: string, message: string): Promise
Wire SMS into the USSD handler
Update src/ussd/handler.ts to call the SMS service after completed transactions. In the airtime confirm branch (where sendAirtime succeeds), after returning the END response, call airtimeSmsReceipt(phoneNumber, steps[1]) — fire and forget (do not await in the response path; use .catch(console.error) to avoid unhandled rejections). In the payment confirm branch (where recordPayment resolves), call paymentSmsReceipt(phoneNumber, meterNo, amount, reference) in the same fire-and-forget pattern. Important: the USSD response must be sent back to AT before the SMS call completes. Make sure res.send() is called before the async SMS functions.
Handle session cleanup
Sessions can be abandoned — users dial and then close the app without pressing END. Add a cleanup timer:
In src/ussd/handler.ts, after storing data in the session Map, also store a cleanup timer: setTimeout(() => sessionMap.delete(sessionId), 5 * 60 * 1000). If a new request arrives for an existing sessionId, clear the old timer and set a new one. This prevents unbounded memory growth from abandoned sessions.
Prepare for Railway deployment
Create a Procfile:
web: node dist/index.js
Add a build script to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node src/index.ts"
}
}
Make sure the server binds to process.env.PORT — Railway assigns the port dynamically. Verify your src/index.ts uses process.env.PORT ?? 3000.
Deploy to Railway
- Push your project to GitHub (create a repo at github.com/new)
- Go to railway.app and sign in with GitHub
- Click New Project → Deploy from GitHub repo and select your repo
- Railway will detect Node.js and run
npm run buildthennpm start - Once deployed, go to Settings → Domains and click Generate Domain to get a public URL
Add your environment variables in Railway's Variables tab — copy everything from your .env file except PORT (Railway sets that automatically):
AT_API_KEY=...
AT_USERNAME=sandbox
AT_SHORTCODE=*384*12345#
SUPABASE_URL=...
SUPABASE_SERVICE_KEY=...
Update the Africa's Talking callback URL
Go to the AT sandbox USSD channel settings and update the Callback URL to your Railway domain:
https://your-app.railway.app/ussd
Test the full flow in the AT simulator one more time using the Railway URL — no ngrok needed.
Switch to production (when you're ready)
When you're ready to go live on a real shortcode:
- In the AT dashboard, go to Production and apply for a shortcode
- Update
AT_USERNAMEin Railway variables fromsandboxto your AT username - Update
AT_API_KEYto your production API key - Update
AT_SHORTCODEto your registered production shortcode
Africa's Talking production shortcodes are country-specific and require regulatory approval. For Ghana, contact the Ghana Communications Authority (GCA) through AT's partnership team. The sandbox is fully functional for testing — there is no rush to go live until you're ready.
What you've built
Over this four-part series you've built a production-ready USSD service that:
- Serves interactive multi-level menus to any phone (feature phone or smartphone)
- Queries real account and meter data from Supabase
- Sends real airtime via the AT Airtime API
- Records payments to a database
- Sends SMS receipts after every transaction
- Runs 24/7 on Railway
The complete source is on GitHub. You can extend it with more service types, a web admin dashboard, or automated bill reminders via scheduled SMS.
BlocWeave