Build
KoboToolbox guide
REST Services post each submission to a small relay that rewards it.
How it fits together
KoboToolbox can post each submission to a URL, but it cannot sign a request the way Jusa's completions endpoint
wants or hold a secret key safely. So a small relay of yours sits between them: KoboToolbox posts to it, it checks the
post really came from your form, and it calls POST /rewards, keyed on the submission so each is paid
once, forever.
- Make a program (dashboard or
POST /programs) with the budget, the amount andper_recipient={"lifetime": 1}if each person is paid once. - Make a restricted key with only
rewards:write, and give it to the relay. If it ever leaks, it can reward from that program's budget and do nothing else. - Add a question for the respondent's mobile number to the form (and their meter, if you pay ZESA).
In KoboToolbox
- Open the project, then Settings → REST Services, and register a service pointing at your relay,
for example
https://relay.example.org/kobo, sending JSON. - Add a custom HTTP header with a long random value, for example
X-Relay-Token: 3f9c…. Kobo cannot sign the body, so this shared value is how the relay knows the post is yours; keep it out of the form. - Each submission arrives as JSON with your question names as keys and Kobo's own fields, among them
_uuid, a unique id for the submission. That is thecompletion_id.
The relay
import hmac, os, jusa from flask import Flask, request, abort jusa.api_key = os.environ["JUSA_REWARDS_KEY"] # jusa_rk_… with rewards:write only app = Flask(__name__) @app.post("/kobo") def kobo(): if not hmac.compare_digest(request.headers.get("X-Relay-Token", ""), os.environ["RELAY_TOKEN"]): abort(403) s = request.get_json() reward = jusa.Reward.create( program=os.environ["JUSA_PROGRAM"], recipient={"phone": s["respondent_phone"]}, # your question's name completion_id=s["_uuid"], metadata={"form": str(s.get("_xform_id_string", ""))}, idempotency_key=f"kobo-{s['_uuid']}", ) return {"reward": reward.id}, 200
- Kobo retries a failed post;
completion_idmakes a repeat the same reward (200), never a second one. - Return
2xxeven when Jusa refuses (a cap, a blocked number): the refusal is final for that submission, andGET /rewards?completion_id=…shows it. Return5xxonly when Jusa could not be reached, so Kobo tries again. - Test first with a test key and a test program: numbers ending
0002fail,0010are held for review.
No phone number on the form?
Mint reward links before fieldwork (POST /reward-links {"count": 500, "bind": "otp"}) and have
enumerators hand out the QR codes; each respondent claims on their own phone. See the
claim cards recipe.