Start
SDKs and tools
Python, Node, the jusa CLI, Postman, the OpenAPI document and MCP.
Python: jusa
No dependencies beyond the standard library; Python 3.9 and later.
pip install jusa
import jusa jusa.api_key = "jusa_sk_test_…" send = jusa.Send.create(product="airtime-usd", target="0772123456", amount="1.00", client_reference="order-8812") client = jusa.Jusa("jusa_sk_test_…") # or one client per key for s in client.sends.list(status="delivered", created={"gte": 1759230000}).auto_paging_iter(): print(s.id, s.amount) try: client.sends.create(target="0772123456", amount="500.00") except jusa.InsufficientCreditError as e: print(e.code, e.request_id)
- Every endpoint is a method:
client.<resource>.<action>(ids…, **params), generated from the OpenAPI document. Each resource is also a class on the module:jusa.Reward.create(…). - Idempotency keys are automatic on every operation that takes one, and reused across that call's
retries. They only protect retries inside one process: pass
idempotency_key=(an order or submission id) to be safe across restarts. - Retries on connection errors, 429 and 5xx, with backoff and
Retry-After(max_retries=2). - Typed errors:
InvalidRequestError,AuthenticationError,InsufficientCreditError,PermissionDeniedError,NotFoundError,ConflictError,IdempotencyError,RateLimitError,ProviderUnavailableError,APIError,APIConnectionError, alljusa.JusaErrorwithcode,paramandrequest_id. jusa.Webhook.construct_event(raw_body, signature_header, secret, tolerance=300)verifies and parses a delivery;event.idis what you dedupe on.- Parameters named like Python keywords take a trailing underscore:
from_="2026-09-01".
Node and TypeScript: @jusa/node
Node 18 and later, no dependencies, typed from the OpenAPI document.
npm install @jusa/node
import Jusa, { InsufficientCreditError } from '@jusa/node'; const jusa = new Jusa(process.env.JUSA_SECRET_KEY); const send = await jusa.sends.create( { product: 'airtime-usd', target: '0772123456', amount: '1.00' }, { idempotencyKey: 'order-8812' }, ); const event = Jusa.webhooks.constructEvent(rawBody, req.headers['jusa-signature'], secret);
The jusa CLI
Comes with the Python package.
jusa login # store a key (a test key while you build) jusa listen --forward-to localhost:8000/jusa/webhooks jusa listen --events 'send.*,reward.*' # only these jusa trigger send.delivered # fire a sample event (test mode) jusa logs tail # your requests as they happen jusa status # ZESA and the networks
listen needs no public URL: it polls GET /events and forwards each new event to your local
URL, signed with a whsec_cli_… secret it prints, so your handler verifies it exactly as it will verify
Jusa's own deliveries.
Postman and OpenAPI
- OpenAPI 3.1 document: every endpoint, request body and object.
- Postman collection: set
apiKeyto a test key; every request that moves money carriesIdempotency-Key: {{$guid}}.
MCP for AI agents
An MCP server at https://jusa.localhost.co.zw/dev/v1/mcp (JSON-RPC 2.0 over HTTP, stateless) for agent keys
(jusa_ak_…). Tools: send_airtime, buy_zesa, send_data (once live
data bundles ship; simulated in test mode), confirm_meter, lookup_phone,
quote_zesa, create_reward, get_balance, get_send,
list_sends, cancel_send, resend_token, list_products,
list_programs, list_recipients, get_status, zesa_status and
search_docs; the key's scopes decide which it sees. An agent never mints reward links. An agent spends
within its rolling 24-hour threshold and to numbers it knows; anything more waits for a person, who approves with
their payment PIN. Live agent keys are off until an owner or admin turns them on.
In the browser
Only publishable keys (jusa_pk_…) work from a web page: the catalog, status, reward-link claims and
check-ins. Secret, restricted and agent keys sent from a browser are refused. The widget
/static/developers/jusa.js claims a link (Jusa.claim(el, {publishableKey, token})) or picks a
product for your server to sell with a checkout session (Jusa.buy).