66[ ![ License] ( https://img.shields.io/pypi/l/helliomessaging.svg )] ( LICENSE )
77
88Python client for the [ Hellio Messaging] ( https://helliomessaging.com ) API v1:
9- ** SMS** , ** OTP** (SMS / email / voice), ** Voice broadcasts** , ** Number Lookup (HLR)** ,
10- ** Email Verification** , and ** Webhooks** . Fully type-hinted and synchronous.
9+ ** SMS** , ** OTP** (SMS / email / voice), ** Voice broadcasts** , ** USSD** ,
10+ ** Number Lookup (HLR)** , ** Email Verification** , and ** Webhooks** . Fully
11+ type-hinted and synchronous.
1112
1213## Install
1314``` bash
@@ -90,6 +91,81 @@ client.webhooks()
9091client.delete_webhook(1 )
9192```
9293
94+ ## USSD
95+ USSD 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 ` ).
100+
101+ ``` python
102+ from hellio import Hellio
103+
104+ client = Hellio(token = " your-token-here" )
105+
106+ # Pricing and availability
107+ client.ussd.pricing() # session prices per network + extension rents
108+ client.ussd.availability(100 ) # {'data': {'valid': True, 'available': True, 'monthly_price': '50.00'}}
109+
110+ # Apps (the callback endpoints Hellio POSTs session steps to)
111+ client.ussd.apps() # list (pass cursor="..." for the next page)
112+ app = client.ussd.create_app(" Airtime Top-up" , " https://your-app.com/ussd" )
113+ app_id = app[" data" ][" id" ]
114+ client.ussd.update_app(app_id, name = " Airtime" , active = True )
115+ client.ussd.delete_app(app_id)
116+
117+ # Extensions (short-code suffixes you rent and bind to an app)
118+ client.ussd.extensions()
119+ ext = client.ussd.rent_extension(100 , app_id = app_id)
120+ client.ussd.release_extension(ext[" data" ][" id" ])
121+
122+ # Sessions
123+ client.ussd.sessions(status = " ended" ) # optional status filter
124+ client.ussd.session(" sess_ref_123" )
125+
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}}
134+ ```
135+
136+ Renting an extension that has just been taken raises ` ConflictError ` (409); an
137+ empty balance raises ` InsufficientBalanceError ` (402):
138+
139+ ``` python
140+ from hellio import ConflictError, InsufficientBalanceError
141+
142+ try :
143+ client.ussd.rent_extension(100 )
144+ except ConflictError:
145+ ... # someone else rented it first; try another code
146+ except InsufficientBalanceError:
147+ ... # top up
148+ ```
149+
150+ ### Inbound callback
151+ When a subscriber uses your extension, Hellio POSTs
152+ ` { sessionId, msisdn, serviceCode, input, sequence, mode } ` to the app's
153+ ` callback_url ` , signed with an ` X-Hellio-Signature ` header
154+ (` HMAC-SHA256(rawBody, app.secret) ` ). Verify the signature, then return
155+ ` { message, action } ` where ` action ` is ` "continue" ` or ` "end" ` :
156+
157+ ``` python
158+ import hashlib
159+ import hmac
160+
161+ def handle_ussd (raw_body : bytes , signature : str , secret : str ) -> dict :
162+ expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
163+ if not hmac.compare_digest(expected, signature):
164+ raise ValueError (" bad signature" )
165+ # ... branch on the parsed payload ...
166+ return {" message" : " Welcome to Airtime Top-up" , " action" : " continue" }
167+ ```
168+
93169## Error handling
94170Non-2xx responses raise typed exceptions (all extend ` HellioError ` ). Each error
95171carries ` message ` , ` status_code ` , and ` response ` (the parsed body); validation
@@ -99,6 +175,7 @@ errors also expose `errors`.
99175| ---| ---|
100176| ` InvalidApiTokenError ` | 401 |
101177| ` InsufficientBalanceError ` | 402 |
178+ | ` ConflictError ` | 409 |
102179| ` ValidationError ` (` .errors ` ) | 422 |
103180| ` RateLimitError ` | 429 |
104181| ` HellioError ` | other |
0 commit comments