@@ -93,10 +93,19 @@ client.delete_webhook(1)
9393
9494## USSD
9595USSD lives under the ` client.ussd ` namespace. Needs a token with the ` ussd `
96- ability. You rent an ** extension** (a short-code suffix, e.g. ` *920*100# ` ), point
97- it at a USSD ** app** whose ` callback_url ` Hellio calls on every step, and can
98- inspect ** sessions** or ` simulate ` a step without dialling the real code. List
99- endpoints are cursor-paginated (` data ` array + ` meta.next_cursor ` ).
96+ ability. You build a USSD ** app** whose ` callback_url ` Hellio calls on every
97+ step, ` simulate ` it in the sandbox by ` app_id ` , rent an ** extension** (a
98+ short-code suffix, e.g. ` *920*100# ` ) from your dedicated USSD balance, then flip
99+ the app to ** live** mode. You can also inspect ** sessions** . List endpoints are
100+ cursor-paginated (` data ` array + ` meta.next_cursor ` ).
101+
102+ Every app has a ** test** and a ** live** mode. A new app starts in ` test ` and
103+ carries two secrets, ` test_secret ` (` ussk_test_... ` ) and ` live_secret `
104+ (` ussk_live_... ` ); the active one is the secret for the current ` mode ` .
105+ Simulation always runs in the sandbox (test mode): no charge, no extension
106+ needed. Going live requires a purchased extension, and live USSD sessions are
107+ billed to a dedicated USSD balance, separate from your SMS credit and main
108+ wallet.
100109
101110``` python
102111from hellio import Hellio
@@ -107,51 +116,67 @@ client = Hellio(token="your-token-here")
107116client.ussd.pricing() # session prices per network + extension rents
108117client.ussd.availability(100 ) # {'data': {'valid': True, 'available': True, 'monthly_price': '50.00'}}
109118
110- # Apps (the callback endpoints Hellio POSTs session steps to)
111- client.ussd.apps() # list (pass cursor="..." for the next page)
119+ # 1. Create an app (starts in test mode; ids are UUID strings)
112120app = client.ussd.create_app(" Airtime Top-up" , " https://your-app.com/ussd" )
113- app_id = app[" data" ][" id" ]
121+ app_id = app[" data" ][" id" ] # e.g. "9b1f...": a UUID string
122+ # app["data"] also has: mode, test_secret, live_secret, is_live, active
123+ client.ussd.apps() # list (pass cursor="..." for the next page)
114124client.ussd.update_app(app_id, name = " Airtime" , active = True )
115- client.ussd.delete_app(app_id)
116125
117- # Extensions (short-code suffixes you rent and bind to an app)
126+ # 2. Simulate a subscriber step in the sandbox (no dialling, no charge)
127+ client.ussd.simulate(
128+ app_id = app_id,
129+ session_id = " sess-1" ,
130+ msisdn = " 233241234567" ,
131+ input = " 1" ,
132+ new_session = True ,
133+ )
134+ # -> {'data': {'message': 'Welcome...', 'action': 'continue', 'continue': True}}
135+
136+ # 3. Rent an extension from your USSD balance and bind it to the app
118137client.ussd.extensions()
119138ext = client.ussd.rent_extension(100 , app_id = app_id)
120- client.ussd.release_extension(ext[" data" ][" id" ])
139+
140+ # 4. Go live (needs a purchased extension) and rotate secrets as needed
141+ client.ussd.set_mode(app_id, " live" )
142+ client.ussd.rotate_secret(app_id, " live" )
121143
122144# Sessions
123145client.ussd.sessions(status = " ended" ) # optional status filter
124146client.ussd.session(" sess_ref_123" )
125147
126- # Simulate a subscriber step against your callback (no real dialling)
127- client.ussd.simulate(
128- msisdn = " 233241234567" ,
129- service_code = " *920*100#" ,
130- user_input = " 1" ,
131- new_session = True ,
132- )
133- # -> {'data': {'message': 'Welcome...', 'action': 'continue', 'continue': True}}
148+ # Teardown
149+ client.ussd.release_extension(ext[" data" ][" id" ])
150+ client.ussd.delete_app(app_id)
134151```
135152
136- Renting an extension that has just been taken raises ` ConflictError ` (409); an
137- empty balance raises ` InsufficientBalanceError ` (402):
153+ Renting an extension that has just been taken raises ` ConflictError ` (409); too
154+ low a USSD balance raises ` InsufficientBalanceError ` (402,
155+ ` insufficient_ussd_balance ` ); switching to live before an extension is purchased
156+ raises ` ExtensionRequiredError ` (402, ` extension_required ` ):
138157
139158``` python
140- from hellio import ConflictError, InsufficientBalanceError
159+ from hellio import ConflictError, ExtensionRequiredError, InsufficientBalanceError
141160
142161try :
143162 client.ussd.rent_extension(100 )
144163except ConflictError:
145164 ... # someone else rented it first; try another code
146165except InsufficientBalanceError:
147- ... # top up
166+ ... # top up your USSD balance
167+
168+ try :
169+ client.ussd.set_mode(app_id, " live" )
170+ except ExtensionRequiredError:
171+ ... # rent an extension for the app first
148172```
149173
150174### Inbound callback
151175When a subscriber uses your extension, Hellio POSTs
152176` { sessionId, msisdn, serviceCode, input, sequence, mode } ` to the app's
153177` callback_url ` , signed with an ` X-Hellio-Signature ` header
154- (` HMAC-SHA256(rawBody, app.secret) ` ). Verify the signature, then return
178+ (` HMAC-SHA256(rawBody, secret) ` , where ` secret ` is the app's ` test_secret ` or
179+ ` live_secret ` for the ` mode ` on the request). Verify the signature, then return
155180` { message, action } ` where ` action ` is ` "continue" ` or ` "end" ` :
156181
157182``` python
@@ -174,7 +199,8 @@ errors also expose `errors`.
174199| Exception | Status |
175200| ---| ---|
176201| ` InvalidApiTokenError ` | 401 |
177- | ` InsufficientBalanceError ` | 402 |
202+ | ` InsufficientBalanceError ` | 402 (` insufficient_ussd_balance ` ) |
203+ | ` ExtensionRequiredError ` | 402 (` extension_required ` ) |
178204| ` ConflictError ` | 409 |
179205| ` ValidationError ` (` .errors ` ) | 422 |
180206| ` RateLimitError ` | 429 |
0 commit comments