Skip to content

Commit 6ce38b1

Browse files
committed
feat: finished docs and added GitHub deploy yml
1 parent 372c0cd commit 6ce38b1

18 files changed

Lines changed: 2688 additions & 21 deletions

.github/workflows/deploy-docs.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Deploy Docs to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
paths:
7+
- 'docs/**'
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
pages: write
13+
id-token: write
14+
15+
concurrency:
16+
group: "pages"
17+
cancel-in-progress: false
18+
19+
jobs:
20+
deploy:
21+
environment:
22+
name: github-pages
23+
url: ${{ steps.deployment.outputs.page_url }}
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: '18'
33+
cache: 'npm'
34+
cache-dependency-path: './docs/package-lock.json'
35+
36+
- name: Install dependencies
37+
run: |
38+
cd docs
39+
npm ci
40+
41+
- name: Build website
42+
run: |
43+
cd docs
44+
npm run build
45+
46+
- name: Setup Pages
47+
uses: actions/configure-pages@v4
48+
49+
- name: Upload artifact
50+
uses: actions/upload-pages-artifact@v3
51+
with:
52+
path: './docs/build'
53+
54+
- name: Deploy to GitHub Pages
55+
id: deployment
56+
uses: actions/deploy-pages@v4

docs/docs/account-balance.mdx

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
---
2+
title: Account Balance
3+
description: Query organization account balances and handle result and timeout callbacks from the M-Pesa Account Balance API.
4+
---
5+
6+
import SectionHeader from '@site/docs/components/SectionHeader';
7+
import CodeBlock from '@site/docs/components/CodeBlock';
8+
import Alert from '@site/docs/components/Alert';
9+
import ParametersTable from '@site/docs/components/ParametersTable';
10+
import Tabs from '@site/docs/components/Tabs';
11+
12+
<SectionHeader
13+
subtitle="Request account balances, receive asynchronous result notifications, and handle timeout callbacks from Safaricom's Account Balance API."
14+
gradient={true}
15+
size="large"
16+
/>
17+
18+
<Alert type="info" title="What this page covers">
19+
This guide documents the Account Balance flow: how to construct queries, expected acknowledgement responses, how to parse ResultURL callbacks (final balances) and QueueTimeOutURL notifications, and important validation/safety checks to implement on your side.
20+
</Alert>
21+
22+
<SectionHeader title="Parameters Definition" size="small" />
23+
24+
<ParametersTable
25+
parameters={[
26+
{
27+
name: "Initiator",
28+
type: "str",
29+
dataType: "String",
30+
required: true,
31+
description: "Username used to initiate the request (API initiator identity)."
32+
},
33+
{
34+
name: "SecurityCredential",
35+
type: "str",
36+
dataType: "String",
37+
required: true,
38+
description: "Encrypted security credential (as required by Safaricom)."
39+
},
40+
{
41+
name: "CommandID",
42+
type: "str",
43+
dataType: "String",
44+
required: true,
45+
description: "Transaction type. Default: 'AccountBalance'."
46+
},
47+
{
48+
name: "PartyA",
49+
type: "int",
50+
dataType: "Integer",
51+
required: true,
52+
description: "Organization shortcode, till number or MSISDN depending on IdentifierType."
53+
},
54+
{
55+
name: "IdentifierType",
56+
type: "int",
57+
dataType: "Integer",
58+
required: true,
59+
description: "Type of PartyA. Allowed values: 1 (MSISDN), 2 (Till number), 4 (Short code)."
60+
},
61+
{
62+
name: "Remarks",
63+
type: "str",
64+
dataType: "String",
65+
required: true,
66+
description: "Freeform comments for the transaction (must not exceed 100 characters)."
67+
},
68+
{
69+
name: "QueueTimeOutURL",
70+
type: "str",
71+
dataType: "String",
72+
required: true,
73+
description: "Callback URL for timeout notifications (QueueTimeOutURL)."
74+
},
75+
{
76+
name: "ResultURL",
77+
type: "str",
78+
dataType: "String",
79+
required: true,
80+
description: "Callback URL where final account balance result will be posted."
81+
}
82+
]}
83+
/>
84+
85+
<SectionHeader title="Responses & Result Notifications" size="small" />
86+
87+
<ParametersTable
88+
parameters={[
89+
{
90+
name: "OriginatorConversationID",
91+
type: "str",
92+
dataType: "String",
93+
required: false,
94+
description: "Unique identifier for the original request acknowledgement."
95+
},
96+
{
97+
name: "ConversationID",
98+
type: "str",
99+
dataType: "String",
100+
required: false,
101+
description: "Unique transaction identifier returned by M-Pesa."
102+
},
103+
{
104+
name: "ResponseCode",
105+
type: "str|int",
106+
dataType: "String/Integer",
107+
required: true,
108+
description: "Acknowledgement status code. '0' indicates acceptance of the request."
109+
},
110+
{
111+
name: "ResponseDescription",
112+
type: "str",
113+
dataType: "String",
114+
required: true,
115+
description: "Human readable acknowledgement message."
116+
},
117+
{
118+
name: "Result.ResultType / Result.ResultCode / Result.ResultDesc",
119+
type: "int|int|str",
120+
dataType: "Integer/Integer/String",
121+
required: true,
122+
description: "Result metadata posted to ResultURL. ResultType=0 => success; ResultCode=0 => success."
123+
},
124+
{
125+
name: "Result.ResultParameter.ResultParameters",
126+
type: "list",
127+
dataType: "Array",
128+
required: false,
129+
description: "List of key/value result parameters. 'AccountBalance' key contains pipe- and ampersand-delimited balance blobs for accounts."
130+
},
131+
{
132+
name: "ReferenceData.ReferenceItem",
133+
type: "object",
134+
dataType: "Object",
135+
required: false,
136+
description: "Reference item included in callbacks (e.g., original QueueTimeoutURL)."
137+
}
138+
]}
139+
/>
140+
141+
<SectionHeader title="Overview" size="small" subtitle="Query lifecycle: send AccountBalance request → receive immediate ack → process asynchronous result or timeout." />
142+
143+
<Alert type="info" title="Implementation Options">
144+
- Use the high-level MpesaClient/BalanceService facade for token management, header injection and typed Pydantic models.
145+
- Use the lower-level AccountBalance class with a TokenManager and HttpClient if you need full control over request construction, middleware and error handling.
146+
</Alert>
147+
148+
<Tabs defaultValue={0}>
149+
<Tabs.TabPane label="MpesaClient (Recommended)" icon="🚀">
150+
<Alert type="info" title="Why use the facade">
151+
The facade handles authentication and returns typed models (acknowledgement and parsed callbacks). Use it for concise integration in most applications.
152+
</Alert>
153+
154+
<SectionHeader title="Quick Setup" size="small" />
155+
156+
<CodeBlock language="python" title="Python (example)">
157+
{`
158+
# Requires: mpesakit, fastapi (for webhook example)
159+
from mpesakit import MpesaClient
160+
from mpesakit.account_balance import (
161+
AccountBalanceResultCallback,
162+
AccountBalanceTimeoutCallback,
163+
AccountBalanceIdentifierType,
164+
)
165+
from mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
166+
167+
# ---- Client / Request (synchronous example) ----
168+
client = MpesaClient(consumer_key="YOUR_KEY", consumer_secret="YOUR_SECRET", environment="sandbox")
169+
170+
resp = client.balance.query(
171+
initiator="apiuser",
172+
security_credential="ENCRYPTED_SECURITY_CREDENTIAL",
173+
party_a=600000,
174+
identifier_type=AccountBalanceIdentifierType.SHORT_CODE,
175+
remarks="Balance inquiry",
176+
result_url="https://your.service/webhook/account-balance/result",
177+
queue_timeout_url="https://your.service/webhook/account-balance/timeout",
178+
)
179+
180+
if resp.is_successful():
181+
print("Acknowledgement accepted:", resp.ConversationID or resp.OriginatorConversationID)
182+
else:
183+
print("Acknowledgement failed:", resp.ResponseDescription)
184+
185+
`}
186+
</CodeBlock>
187+
</Tabs.TabPane>
188+
189+
<Tabs.TabPane label="Direct API" icon="">
190+
<Alert type="warning" title="Advanced Usage">
191+
Use the direct AccountBalance service with explicit TokenManager and HttpClient if you need custom request shaping, retry policies or observability hooks.
192+
</Alert>
193+
194+
<SectionHeader title="Direct Implementation" size="small" />
195+
196+
<CodeBlock language="python" title="Python (example)">
197+
{`
198+
# Build an AccountBalanceRequest and call the low-level service.
199+
# The service posts to /mpesa/accountbalance/v1/query and returns an acknowledgement.
200+
request = AccountBalanceRequest(...)
201+
response = account_balance.query(request)
202+
# response is an AccountBalanceResponse model.
203+
`}
204+
</CodeBlock>
205+
</Tabs.TabPane>
206+
</Tabs>
207+
208+
<SectionHeader title="Webhook Handling (Result & Timeout)" size="small" />
209+
210+
<CodeBlock language="python" title="Webhook Example (FastAPI)">
211+
{`
212+
# Example: FastAPI endpoints for Account Balance Result & Timeout callbacks
213+
from fastapi import FastAPI, Request, HTTPException
214+
from mpesakit.account_balance import (
215+
AccountBalanceResultCallback,
216+
AccountBalanceTimeoutCallback,
217+
AccountBalanceResultCallbackResponse,
218+
AccountBalanceTimeoutCallbackResponse,
219+
)
220+
from mpesakit.security.ip_whitelist import is_mpesa_ip_allowed
221+
import logging
222+
223+
app = FastAPI()
224+
logger = logging.getLogger("mpesa.account_balance")
225+
226+
227+
async def _caller_ip(request: Request) -> str:
228+
return (request.headers.get("x-forwarded-for") or request.client.host).split(",")[0].strip()
229+
230+
231+
@app.post("/webhook/account-balance/result")
232+
async def account_balance_result(request: Request):
233+
payload = await request.json()
234+
caller_ip = await _caller_ip(request)
235+
if not is_mpesa_ip_allowed(caller_ip):
236+
raise HTTPException(status_code=403, detail="forbidden")
237+
238+
# Validate and parse incoming payload into project models
239+
try:
240+
callback = AccountBalanceResultCallback(**payload)
241+
except Exception as exc:
242+
logger.exception("Invalid AccountBalance result payload")
243+
return AccountBalanceResultCallbackResponse(ResultCode=1, ResultDesc=f"Invalid payload: {exc}")
244+
245+
response = callback.model_dump(mode="json")
246+
logger.info("AccountBalance result received: %s", response)
247+
248+
return AccountBalanceResultCallbackResponse() # defaults to ResultCode=0, ResultDesc="Result received and processed successfully."
249+
250+
251+
@app.post("/webhook/account-balance/timeout")
252+
async def account_balance_timeout(request: Request):
253+
payload = await request.json()
254+
caller_ip = await _caller_ip(request)
255+
if not is_mpesa_ip_allowed(caller_ip):
256+
raise HTTPException(status_code=403, detail="forbidden")
257+
258+
try:
259+
callback = AccountBalanceTimeoutCallback(**payload)
260+
except Exception as exc:
261+
logger.exception("Invalid AccountBalance timeout payload")
262+
return AccountBalanceTimeoutCallbackResponse(ResultCode=1, ResultDesc=f"Invalid payload: {exc}")
263+
264+
response = callback.model_dump(mode="json")
265+
logger.warning("AccountBalance timeout received: %s", response)
266+
267+
# Quick typed acknowledgement to stop retries
268+
return AccountBalanceTimeoutCallbackResponse() # defaults to ResultCode=0, ResultDesc="Timeout notification received and processed successfully."
269+
`}
270+
</CodeBlock>
271+
272+
<SectionHeader title="Important behaviors & validations" size="small" />
273+
274+
<Alert type="warning" title="IdentifierType & Remarks validation">
275+
- IdentifierType must be one of the supported enum values (1=MSISDN, 2=Till number, 4=Short code). Invalid values should be rejected before sending requests.
276+
- Remarks must not exceed 100 characters; the client/model will raise validation errors if this rule is violated.
277+
</Alert>
278+
279+
<Alert type="info" title="Acknowledgement semantics">
280+
- The initial /query call returns an acknowledgement (ResponseCode/ResponseDescription). This does not contain the final balance.
281+
- Final account balances are delivered asynchronously to your ResultURL (ResultType/ResultCode = 0 indicates success).
282+
</Alert>
283+
284+
<SectionHeader title="Response helpers" size="small" />
285+
286+
- A returned AccountBalanceResponse model exposes:
287+
- is_successful() helper that treats any all-zero ResponseCode string (e.g., "0" or "000") as success.
288+
- Result callbacks are parsed into AccountBalanceResultCallback with:
289+
- ResultParameter.ResultParameters list containing an 'AccountBalance' entry holding a delimited balance string.
290+
291+
292+
<SectionHeader title="Testing & Expected Behaviors" size="small" />
293+
294+
- Query:
295+
- Should return an AccountBalanceResponse acknowledgement. Confirm `is_successful()` for `ResponseCode == 0`.
296+
- HTTP client must receive the correct path and headers (`Authorization Bearer token + JSON content-type`).
297+
298+
- Result Callback:
299+
- Parse Result.ResultParameter.ResultParameters and extract the AccountBalance key. Persist parsed balances for reconciliation.
300+
- Ensure your handler returns the expected acknowledgement payload quickly so provider retries are avoided.
301+
302+
- Timeout Callback:
303+
- QueueTimeOutURL receives a Result with ResultType=1; handle accordingly (mark request as timed out, alert operations).
304+
305+
<SectionHeader title="Next Steps" size="small" />
306+
307+
<Alert type="info" title="Go-live checklist">
308+
- Implement secure callback endpoints (IP restrictions, TLS).
309+
- Persist both acknowledgements and final result notifications for audit and reconciliation.
310+
- Add observability & alerting for timeout notifications and unexpected result codes.
311+
</Alert>
312+
313+
## Related Documentation
314+
315+
- [📡 Webhook Setup Guide](/webhooks-best-practices) - Best practices for building reliable endpoints
316+
- [🏗️ Production Setup](/production) - Go-live checklist, security and monitoring

0 commit comments

Comments
 (0)