Build

SurveyCTO guide

A form webhook posts each submission to a relay that rewards it.

How it fits together

SurveyCTO 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: SurveyCTO 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.

  1. Make a program (dashboard or POST /programs) with the budget, the amount and per_recipient={"lifetime": 1} if each person is paid once.
  2. 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.
  3. Add a question for the respondent's mobile number to the form (and their meter, if you pay ZESA).

In SurveyCTO

  1. In your server console, set up a webhook for the form (SurveyCTO's data publishing to a URL), pointing at your relay, for example https://relay.example.org/surveycto?token=3f9c…. The long random token in the URL is how the relay knows the post is yours.
  2. Each submission arrives as JSON with your field names and SurveyCTO's own fields, among them KEY (uuid:…), unique per submission. That is the completion_id.
  3. Keep the phone field unencrypted, or the relay cannot read it.

The relay

@app.post("/surveycto")
def surveycto():
    if not hmac.compare_digest(request.args.get("token", ""), os.environ["RELAY_TOKEN"]):
        abort(403)
    s = request.get_json()
    submission = s["KEY"]                               # uuid:…
    jusa.Reward.create(
        program=os.environ["JUSA_PROGRAM"],
        recipient={"phone": s["phone"]},
        completion_id=submission,
        idempotency_key=f"scto-{submission}",
    )
    return "", 200

The same rules as any relay: a repeat is one reward, a refusal is final (answer 2xx), and an unreachable Jusa is the only reason to answer 5xx.

Paying ZESA instead

Add a meter question and a token phone: recipient={"meter": s["meter"], "token_phone": s["phone"]}, and let the program's choices include zesa. The meter is confirmed with ZESA before anything is charged.