Skip to the page
Sections

Code only

Submit a code with a phone number and consent.

For a campaign where a code is all a consumer needs: capabilities.receipt_required is false. One call enters them.

Note:

Minted codes and Seebait codes

The same call serves campaigns whose codes we mint and campaigns on Seebait codes. They differ in two ways. For a Seebait campaign the code check answers unsupported, because only a submission can tell. And a Seebait submission can come back unknown with checking when Seebait does not answer in time: show the checking state and poll; it settles later. Test both with the sentinels; the replies have the same shape.

1. When the page loads

Ask for the campaign. status tells you which screen to show; messages has the copy for every outcome in the consumer's language.

curl
curl -s "$UTC_API_URL/api/v1/campaign" \
  -H "Authorization: Bearer $UTC_API_KEY"

If the consumer arrived by scanning a QR, the code is in the address. Check it before they fill anything in:

curl
curl -s -X POST "$UTC_API_URL/api/v1/codes/check" \
  -H "Authorization: Bearer $UTC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "UTC-Client-IP: 196.41.60.77" \
  -d '{"code":"TEST-0-ENTERED","phone":"0712345678"}'
  • valid: show the form.
  • invalid or already_used: show message and stop. message_key says which.
  • unsupported: the campaign's codes are checked elsewhere; show the form and let the submission decide.

The check holds nothing. The submission checks the code again.

2. Submit

Mint the Idempotency-Key in the browser when the consumer presses submit, and forward it from your server. Why, and how.

curl
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
  -H "Authorization: Bearer $UTC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"phone":"0712345678","code":"TEST-0-ENTERED","consent":true}'
Your server: app/api/enter/route.ts
import { createClient, isUtcApiError } from "@utc/gateway-client"

const utc = createClient({ baseUrl: process.env.UTC_API_URL!, apiKey: process.env.UTC_API_KEY! })

export async function POST(req: Request) {
  const idempotencyKey = req.headers.get("Idempotency-Key") ?? ""
  const { phone, code, consent } = await req.json()
  try {
    const submission = await utc.submit({ phone, code, consent }, { idempotencyKey })
    return Response.json(submission) // 200 for every outcome about the consumer
  } catch (e) {
    if (!isUtcApiError(e)) throw e
    // The request was wrong, or the API could not be reached.
    return Response.json({ error: { code: e.code, message: e.message, field: e.field } }, { status: e.status || 502 })
  }
}

3. Show the outcome

  • Show message. It is already in the consumer's language, with the campaign's own wording where it has any.
  • Branch on message_key for anything more: a prize screen for instant_win, a "try another code" button for invalid.
  • display_status is the short label: Entered, Checking, Not accepted.
  • If terminal is false, say the page can be closed, and poll.
Poll while `terminal` is false, every `poll_after_seconds`
curl -s "$UTC_API_URL/api/v1/submissions/$SUBMISSION_ID" \
  -H "Authorization: Bearer $UTC_API_KEY"
Or let the client do it
const settled = await utc.pollSubmission(submission.id, { maxSeconds: 60 })

Every reply your page can meet is on Outcomes. To see each one, use the sentinels.

4. Errors

A 4xx means your request was wrong; nothing was recorded. The consumer did nothing wrong, so do not show them error.message: it is written for you. The ones a form can cause:

  • 422 phone_invalid: validate the phone in the form first (+255…, 255… or 0…, then 6 or 7, then eight digits).
  • 422 consent_required: the box was not ticked.
  • 429 rate_limited: show the rate_limited message and wait Retry-After seconds.
  • A timeout or a 5xx after the client's retries: show service_unavailable, keep the form and the key.