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.

  1. 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.

  2. 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_…
  3. 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
  4. Reward each completion

    Your survey tool calls your server; your server calls Jusa. completion_id is required and unique per program, forever: the same submission posted twice is one reward. The Idempotency-Key makes 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=8812

    A refused reward (not enough budget, a cap, a paused program) writes nothing, so the same completion_id can be sent again once the cause is fixed. A reward a fraud rule stops is held and waits in Reviews.

  5. Hear how it went

    Add a webhook endpoint (dashboard, or POST /webhook-endpoints) for reward.*. 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/webhooks polls your events and forwards them, signed, to your machine. See the CLI.

  6. Try every outcome

    In test mode the last four digits of a number choose what happens: 0772550002 is rejected by the network, 0772550005 goes unknown and then lands, 0772550010 is held for review. The full list is on Test mode.

  7. 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 is exhausted, 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.