Outcomes
Every reply a frontend can meet, in one table, and the rule that separates a consumer's outcome from an error in your request.
- About the consumer's code, phone, receipt or the campaign window: HTTP 200 with a submission, a
message_keyand a ledger row. - About the caller's request (the key, the shape, a missing upload, a file's type or size): a 4xx or 5xx with an
error.codeand no ledger row. - 429
rate_limitedis only for caller flood limits: a key over its request ceiling, or too many lookups or code checks. A consumer's daily limit is a 200 withdaily_limit.
Row written is whether the request adds a row to the submissions ledger. Retryable, for an error, means sending the same request again (with the same Idempotency-Key where there is one) can succeed without changing it; for a consumer outcome it means the same code and phone can be submitted again later and get a different result.
About the consumer: HTTP 200
About the consumer's code, phone, receipt or the campaign window: HTTP 200 with a submission, a message_key and a ledger row.
| Condition | HTTP | state | Review status | message_key | error.code | Row written | Retryable |
|---|---|---|---|---|---|---|---|
| The code is good and the campaign needs nothing else | 200 | accepted | none | entered | none | Yes | No |
The code is good and carries an instant prizeinstant_win names the prize. | 200 | accepted | none | instant_win | none | Yes | No |
The code is good and TRA's record of the receipt passes every ruleentries is the number of qualifying items on TRA's record. | 200 | accepted | verified | entered | none | Yes | No |
The code is being checked and has not settled yetPoll GET /api/v1/submissions/{id} as poll_after_seconds says. The sentinel TEST-0-SETTLES does this. | 200 | pending | none | checking | none | Yes | No |
| The upstream did not answer, or answered with something unrecognisedShown to the consumer as "Checking". Poll; do not submit again: a retry returns this same row. | 200 | unknown | none | checking | none | Yes | No |
| The receipt is recorded and waiting for TRA, or for a personTRA did not answer in time, the code is cooling down, a typed receipt needs a second look, or a line on the receipt matches no product. Tell the consumer the page can be closed. | 200 | pending | under_review | receipt_received | none | Yes | No |
| The receipt submission is recorded and its one TRA lookup is runningSeen only by a second request while the first is still in flight and older than the time budget, or on GET. It settles within seconds. | 200 | pending | under_review | checking | none | Yes | No |
| The QR code on the receipt photo could not be readOffer the typed path (a new receipt and a new submission); otherwise a person reviews the photo. | 200 | pending | under_review | receipt_unreadable | none | Yes | No |
| The code is not recognisedAlso a code from a live batch sent with a test key, and the reverse. | 200 | rejected | none | invalid | none | Yes | No |
| The code was already used by an entry that counted | 200 | rejected | none | already_used | none | Yes | No |
| The code is attached to another entry that is still being checkedThe code is released if that entry ends rejected. | 200 | rejected | none | code_held | none | Yes | Yes |
| The phone has reached the campaign's daily limitTomorrow, in the campaign's timezone. | 200 | rejected | none | daily_limit | none | Yes | Yes |
| The phone has reached the campaign's total limit | 200 | rejected | none | entry_limit | none | Yes | No |
| The campaign needs an in-store code and none was sentNative campaigns. With the code from the in-store QR. | 200 | rejected | none | code_required | none | Yes | Yes |
The phone is blocked from enteringSeebait campaigns. Today also a receipt a reviewer declines for the phone's cap: the review path uses blocked where the submit path uses entry_limit. | 200 | rejected | none | blocked | none | Yes | No |
The campaign's window has not openedCheck status on GET /api/v1/campaign before showing the form. | 200 | rejected | none | campaign_not_started | none | Yes | Yes |
| The campaign's window has closed | 200 | rejected | none | campaign_ended | none | Yes | No |
| The campaign is pausedOps paused it; entries resume when it is set live again. | 200 | rejected | none | campaign_paused | none | Yes | Yes |
| The receipt is not from a participating store | 200 | rejected | declined | receipt_wrong_store | none | Yes | No |
| The receipt is dated outside the promotion | 200 | rejected | declined | receipt_outside_window | none | Yes | No |
| No participating product is on the receipt | 200 | rejected | declined | receipt_no_qualifying_product | none | Yes | No |
| The receipt has already been entered | 200 | rejected | declined | receipt_already_used | none | Yes | No |
| TRA has no receipt with that codeWith a corrected code, or a clearer photo, as a new receipt and a new submission. | 200 | rejected | declined | receipt_not_found | none | Yes | Yes |
| The typed time does not match TRA's recordWith the time exactly as printed, as a new receipt and a new submission. | 200 | rejected | declined | receipt_time_mismatch | none | Yes | Yes |
| The phone's entries from receipts have reached the campaign's cap | 200 | rejected | declined | entry_limit | none | Yes | No |
The same Idempotency-Key and the same body were sent beforeThe stored submission comes back as it is now, with the header Idempotent-Replayed: true. | 200 | as stored | as stored | as stored | none | No | Yes |
A new Idempotency-Key, but the same campaign, code and phone as an earlier submission that is pending, unknown or acceptedThe earlier submission comes back with replayed: true. After a rejected one, a new submission is made. | 200 | as stored | as stored | as stored | none | No | Yes |
About your request: an error
About the caller's request (the key, the shape, a missing upload, a file's type or size): a 4xx or 5xx with an error.code and no ledger row.
| Condition | HTTP | state | Review status | message_key | error.code | Row written | Retryable |
|---|---|---|---|---|---|---|---|
No Authorization header was sent | 401 | none | none | none | key_missing | No | No |
The header is not Bearer <key>, or the key is not in the utc_test_… / utc_live_… format | 401 | none | none | none | key_malformed | No | No |
| The key is well formed but is not one of ours | 401 | none | none | none | key_unknown | No | No |
| The key has been revoked | 403 | none | none | none | key_revoked | No | No |
| A live key was used on a deployment that is not productionTest keys work on every deployment, production included. | 401 | none | none | none | key_wrong_environment | No | No |
A UTC-Campaign header names a campaign other than the key's | 422 | none | none | none | campaign_mismatch | No | No |
| A BA agent's session has expirednot yet returnedBA sessions arrive with the BA portal cutover. | 401 | none | none | none | session_expired | No | No |
| A BA agent named a campaign they are not assigned tonot yet returnedBA sessions arrive with the BA portal cutover. | 403 | none | none | none | campaign_not_assigned | No | No |
The body is not JSON, or a field is missing or has the wrong shapeerror.field names the first problem. Also: no code where the campaign needs one; a receipt for a campaign that takes none. | 422 | none | none | none | validation_failed | No | No |
| The phone is not a Tanzanian mobile numberValidate in the form first; this is the caller's request, so no row is written. | 422 | none | none | none | phone_invalid | No | No |
consent is missing or not true | 422 | none | none | none | consent_required | No | No |
POST /api/v1/submissions was sent without an Idempotency-Key | 422 | none | none | none | idempotency_key_required | No | No |
The Idempotency-Key is shorter than 16 or longer than 128 characters | 422 | none | none | none | idempotency_key_invalid | No | No |
The Idempotency-Key was used before with a different body | 422 | none | none | none | idempotency_key_reused | No | No |
A receipt submission with this Idempotency-Key is still being processedWait Retry-After seconds. Past the time budget the stored pending submission is returned instead. | 409 | none | none | none | idempotency_in_progress | No | Yes |
A TEST-0-… code or TEST0… receipt was sent with a live key | 422 | none | none | none | sentinel_with_live_key | No | No |
The id is not a uuid, or names nothing in this key's campaign and environmentAlso a receipt_id that does not exist, and a key whose campaign is not in the database. | 404 | none | none | none | not_found | No | No |
The campaign needs a receipt and the submission has no receipt_id | 422 | none | none | none | receipt_required | No | No |
| The receipt's photo is not in storageAfter the PUT succeeds, with the same Idempotency-Key. | 422 | none | none | none | receipt_not_uploaded | No | Yes |
| The receipt already belongs to a submission | 422 | none | none | none | receipt_already_attached | No | No |
| A typed receipt was sent to a campaign that takes no receipts | 422 | none | none | none | typed_receipt_unsupported | No | No |
size_bytes is over 5 MB, or the stored file is | 413 | none | none | none | file_too_large | No | No |
The stored file is not a JPEG, PNG or WebPA wrong content_type on POST /api/v1/receipts is 422 validation_failed; this is the file itself, found at submission. | 415 | none | none | none | file_wrong_type | No | No |
The key is over its requests-per-minute ceiling, or the lookup or code-check limit was reachedAfter Retry-After seconds. | 429 | none | none | none | rate_limited | No | Yes |
| Something failed on our sideWith the same Idempotency-Key. Quote the request id if it keeps failing. | 500 | none | none | none | internal_error | No | Yes |
| The submission could not be recordednot yet returnedReserved: no call answers with it today. | 503 | none | none | none | ledger_unavailable | No | Yes |
Copy for your page
Five message keys are never the message_key of a submission. They are copy for the frontend to show when the API answers with the matching error, or when the page's own check fails.
| Condition | HTTP | state | Review status | message_key | error.code | Row written | Retryable |
|---|---|---|---|---|---|---|---|
Show when the API answers 422 receipt_not_uploadedThe copy is in messages on GET /api/v1/campaign. | none | none | none | receipt_not_uploaded | none | No | Yes |
| Show when the chosen photo is over the limit, or the API answers 413Check the size in the browser before asking for an upload URL. | none | none | none | file_too_large | none | No | Yes |
| Show when the chosen file is not a JPEG, PNG or WebP, or the API answers 415Check the type in the browser first. | none | none | none | file_wrong_type | none | No | Yes |
Show when the API answers 429Wait Retry-After seconds before letting the consumer try again. | none | none | none | rate_limited | none | No | Yes |
| Show when the API cannot be reached or answers 5xx after the client's retriesKeep the form's values and the Idempotency-Key so the consumer can press submit again. | none | none | none | service_unavailable | none | No | Yes |