Start
Quickstart: reward a survey
Your first reward in ten minutes, in test mode.
The seed use case: a survey tool tells your server that someone finished, and they get airtime for it, once. Everything here runs in test mode first, so nothing real moves until you switch the key.
-
Make your developer organisation
Sign in at the dashboard (phone, identity or password) and choose Create developer organisation. You get a live organisation and a test-mode twin with US$1,000 and ZWG 25,000 of test Jusa Credit, topped back up every day.
-
Make a test key
In Developers → Keys, with the switch on Test, create a secret key. It is shown once: put it in your environment as
JUSA_SECRET_KEY. Test keys need no terms and no top-up.pip install jusa # or: npm install @jusa/node export JUSA_SECRET_KEY=jusa_sk_test_… -
Make a program
A program holds the budget and the rules: how much each reward is, what it may be (airtime, data, ZESA), and the caps.
per_recipient={"lifetime": 1}pays each person once;funding="reserve"sets the whole budget aside now, so it cannot be spent twice.import jusa, os jusa.api_key = os.environ["JUSA_SECRET_KEY"] program = jusa.Program.create(name="Household survey Q4", currency="USD", default_amount="0.50", budget="500.00", funding="reserve", choices=["airtime"], per_recipient={"lifetime": 1}, idempotency_key="program-household-q4") print(program.id, program.secret) # the completions secret is shown once
curl https://jusa.localhost.co.zw/dev/v1/programs -H "Authorization: Bearer $JUSA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Household survey Q4", "currency": "USD", "default_amount": "0.50", "budget": "500.00", "funding": "reserve", "choices": ["airtime"], "per_recipient": {"lifetime": 1}}'
-
Reward each completion
Your survey tool calls your server; your server calls Jusa.
completion_idis required and unique per program, forever: the same submission posted twice is one reward. TheIdempotency-Keymakes a retry after a timeout safe too.from flask import Flask, request app = Flask(__name__) @app.post("/survey/completed") # called by your survey tool def survey_completed(): s = request.json reward = jusa.Reward.create( program="prg_7Hq2…", recipient={"external_id": s["respondent_id"], "phone": s["phone"]}, delivery="direct", # or "link" to let them choose completion_id=s["submission_id"], # required: one reward per submission, forever metadata={"survey": s["form_id"]}, idempotency_key=f"survey-{s['submission_id']}", ) return {"reward": reward.id, "status": reward.status} # pending / held
Or the raw call:
curl https://jusa.localhost.co.zw/dev/v1/rewards -H "Authorization: Bearer jusa_sk_test_…" -H "Idempotency-Key: survey-8812" -d program=prg_7Hq2… -d recipient[phone]=0771230000 -d completion_id=8812A refused reward (not enough budget, a cap, a paused program) writes nothing, so the same
completion_idcan be sent again once the cause is fixed. A reward a fraud rule stops isheldand waits in Reviews. -
Hear how it went
Add a webhook endpoint (dashboard, or
POST /webhook-endpoints) forreward.*. Verify every delivery and dedupe on the event id: that is a must, because a delivery can arrive twice.@app.post("/jusa/webhooks") def jusa_webhook(): event = jusa.Webhook.construct_event( request.data, request.headers["Jusa-Signature"], os.environ["JUSA_WHSEC"]) if already_processed(event.id): # MUST: dedupe on the event id return "", 200 if event.type == "reward.delivered": mark_paid(event.data.object.metadata["survey"], event.data.object.recipient) elif event.type in ("reward.failed", "reward.rejected"): flag_for_follow_up(event.data.object) # budget and credit already returned record_processed(event.id) return "", 200
No public URL while you build?
jusa listen --forward-to localhost:5000/jusa/webhookspolls your events and forwards them, signed, to your machine. See the CLI. -
Try every outcome
In test mode the last four digits of a number choose what happens:
0772550002is rejected by the network,0772550005goesunknownand then lands,0772550010is held for review. The full list is on Test mode. -
Go live
Switch the dashboard to Live and create a live key. The first live key asks an owner or admin to accept the API Terms, Acceptable Use policy and DPA, once. There is no review and no starting cap: top up Jusa Credit by card (the card fee is the payer's) or bank transfer, any amount, and your first live reward goes out at once. Swap the key; nothing else changes.
No server? Two other ways in
Signed completions
If your survey tool can sign a webhook, it can post straight to the program:
POST /programs/{id}/completions, with no key, signed with the program's secret:
Jusa-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "{t}.{raw body}")>, within 5 minutes.
The body takes completion_id (or ref), and phone (or target),
meter, token_phone. Google Forms can do this from Apps Script; see the
Google Forms guide.
Reward links
When the phone number never reaches you (paper, enumerator devices offline, a thank-you page), mint links before
the survey: POST /reward-links {"program": "prg_…", "count": 200, "bind": "otp"}. Each has a
url and a qr_png_url; the respondent claims it on their own phone, and a code texted to that
phone stops a forwarded link being claimed by someone else.
For research, M&E and NGO teams
Everything a funder asks about is on by default:
- One reward per completion, for good (
completion_id), and one per phone when you want it (one_per_phone). - Caps per person per day, week and lifetime, a network match (
require_network) and a throttle per hour. - Fraud review: the same number behind many respondent ids, SIM-farm patterns, a number rewarded by many organisations. Held rewards wait for you in Reviews.
- The budget is money: with
funding="reserve"it is set aside at once; when it runs out the program isexhausted, and it comes back to life when budget returns. - A report a funder accepts:
GET /reports/spend?group_by=program, or an export of every reward as CSV. - Failures cost nothing: a reward that is not delivered returns its budget. A send that might have landed is never paid twice; see settlement.