{
  "openapi": "3.1.0",
  "info": {
    "title": "UTC Manager API",
    "version": "1.0.0",
    "description": "One API for every brand frontend: submit a code with a phone and consent, attach a receipt where the campaign needs one, and let the consumer check their entries. Outcomes about the consumer are HTTP 200 with a `message_key`; only a problem with the caller's request is a 4xx. v1 changes are additive only."
  },
  "servers": [
    {
      "url": "{base}",
      "description": "UTC Manager",
      "variables": {
        "base": {
          "default": "http://localhost:3100",
          "description": "Your `UTC_API_URL`"
        }
      }
    }
  ],
  "security": [
    {
      "campaignKey": []
    }
  ],
  "paths": {
    "/api/v1/campaign": {
      "get": {
        "operationId": "getCampaign",
        "summary": "The campaign this key belongs to",
        "description": "Window, status, limits, capabilities and the whole message catalogue in the request's language. Call it when your page loads to decide what to show: whether a code is needed, whether a receipt is needed, the products and branches to offer.",
        "x-utc-client": "campaign",
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignInfo"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "422": {
            "description": "The request is not valid: `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/codes/check": {
      "post": {
        "operationId": "checkCode",
        "summary": "Pre-check a code before the consumer fills anything in",
        "description": "For campaigns whose codes we mint (`capabilities.precheck` is true). Reads only: nothing is held or redeemed, and the submission checks the code again. A campaign on Seebait codes answers `precheck: \"unsupported\"`, because only a submission can tell. Send `phone` when you have it: the consumer's own already-entered code then reads as `valid`, not `already_used`. Limited to 30 calls per consumer address range per 10 minutes.",
        "x-utc-client": "checkCode",
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/UTCClientIP"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CodeCheckRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodeCheckResponse"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "422": {
            "description": "The request is not valid: `validation_failed`, `sentinel_with_live_key`, `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "validation_failed",
                                "sentinel_with_live_key",
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "validation_failed",
              "sentinel_with_live_key",
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/submissions": {
      "post": {
        "operationId": "createSubmission",
        "summary": "Submit a code with a phone number and consent",
        "description": "The one call that enters a consumer. The campaign's configuration decides where the code is checked and whether a receipt is needed. Every outcome about the consumer (bad code, used code, limit reached, campaign closed, receipt declined) is HTTP 200 with a submission and a `message_key`; only a problem with your request is a 4xx. A second request with the same `Idempotency-Key` and body returns the first submission with `Idempotent-Replayed: true`. A new key with the same code and phone returns the earlier submission with `replayed: true` unless that one was rejected.",
        "x-utc-client": "submit",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyRequired"
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmissionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The submission, whatever its outcome for the consumer",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Submission"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "409": {
            "description": "A request with this Idempotency-Key is still in flight: `idempotency_in_progress`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "idempotency_in_progress"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "idempotency_in_progress"
            ]
          },
          "413": {
            "description": "The file is too large: `file_too_large`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "file_too_large"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "file_too_large"
            ]
          },
          "415": {
            "description": "The file is not an accepted type: `file_wrong_type`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "file_wrong_type"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "file_wrong_type"
            ]
          },
          "422": {
            "description": "The request is not valid: `idempotency_key_required`, `idempotency_key_invalid`, `idempotency_key_reused`, `validation_failed`, `consent_required`, `phone_invalid`, `sentinel_with_live_key`, `receipt_required`, `receipt_not_uploaded`, `receipt_already_attached`, `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "idempotency_key_required",
                                "idempotency_key_invalid",
                                "idempotency_key_reused",
                                "validation_failed",
                                "consent_required",
                                "phone_invalid",
                                "sentinel_with_live_key",
                                "receipt_required",
                                "receipt_not_uploaded",
                                "receipt_already_attached",
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "idempotency_key_required",
              "idempotency_key_invalid",
              "idempotency_key_reused",
              "validation_failed",
              "consent_required",
              "phone_invalid",
              "sentinel_with_live_key",
              "receipt_required",
              "receipt_not_uploaded",
              "receipt_already_attached",
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/submissions/{id}": {
      "get": {
        "operationId": "getSubmission",
        "summary": "The current state of one submission",
        "description": "Poll this while `terminal` is false, waiting `poll_after_seconds` between calls. A submission made with another campaign's key, or with a key of the other environment (test or live), is a 404.",
        "x-utc-client": "getSubmission",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Submission"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "422": {
            "description": "The request is not valid: `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/submissions/lookup": {
      "post": {
        "operationId": "lookupSubmissions",
        "summary": "A phone's submissions: the \"check my entries\" page",
        "description": "The latest 50 submissions for a phone in this campaign, with a reference, the day, a display status, the message and the entries. A phone we have never seen gets the same shape with an empty list. Limited to 5 calls per phone and 30 per consumer address range per 10 minutes, so forward the consumer's address in `UTC-Client-IP`.",
        "x-utc-client": "lookup",
        "parameters": [
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/UTCClientIP"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LookupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResponse"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "422": {
            "description": "The request is not valid: `validation_failed`, `phone_invalid`, `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "validation_failed",
                                "phone_invalid",
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "validation_failed",
              "phone_invalid",
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/receipts": {
      "post": {
        "operationId": "createReceipt",
        "summary": "Start a receipt: a photo to upload, or a typed code and time",
        "description": "`type: \"photo\"` returns a receipt id and a presigned `upload` target; the browser PUTs the file there. `type: \"typed\"` takes the verification code and time printed on the receipt and needs no file. Either way, send the receipt's `id` as `receipt_id` on the submission. With an `Idempotency-Key`, the same key and body returns the same receipt (with a fresh upload URL if the file has not arrived).",
        "x-utc-client": "createReceipt",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReceiptCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "413": {
            "description": "The file is too large: `file_too_large`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "file_too_large"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "file_too_large"
            ]
          },
          "422": {
            "description": "The request is not valid: `idempotency_key_invalid`, `idempotency_key_reused`, `validation_failed`, `typed_receipt_unsupported`, `sentinel_with_live_key`, `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "idempotency_key_invalid",
                                "idempotency_key_reused",
                                "validation_failed",
                                "typed_receipt_unsupported",
                                "sentinel_with_live_key",
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "idempotency_key_invalid",
              "idempotency_key_reused",
              "validation_failed",
              "typed_receipt_unsupported",
              "sentinel_with_live_key",
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/receipts/{id}/upload-url": {
      "post": {
        "operationId": "refreshReceiptUploadUrl",
        "summary": "A fresh upload URL when the first one expired",
        "description": "No body. Returns the receipt with a new `upload` target while the file has not arrived. Once the file is there, `state` is `uploaded` and `upload` is null: the first upload for a receipt is the only one.",
        "x-utc-client": "refreshUploadUrl",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "422": {
            "description": "The request is not valid: `receipt_already_attached`, `validation_failed`, `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "receipt_already_attached",
                                "validation_failed",
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "receipt_already_attached",
              "validation_failed",
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    },
    "/api/v1/receipts/{id}": {
      "get": {
        "operationId": "getReceipt",
        "summary": "Whether the receipt's file has arrived",
        "description": "`state` moves from `awaiting_upload` to `uploaded` when the file is in storage, and to `attached` once a submission uses it. `upload` is always null here.",
        "x-utc-client": "getReceipt",
        "parameters": [
          {
            "$ref": "#/components/parameters/id"
          },
          {
            "$ref": "#/components/parameters/XRequestId"
          },
          {
            "$ref": "#/components/parameters/UTCCampaign"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          },
          {
            "$ref": "#/components/parameters/locale"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "401": {
            "description": "The key is missing or not usable: `key_missing`, `key_malformed`, `key_unknown`, `key_wrong_environment`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_missing",
                                "key_malformed",
                                "key_unknown",
                                "key_wrong_environment"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_wrong_environment"
            ]
          },
          "403": {
            "description": "The key may not do this: `key_revoked`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "key_revoked"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "key_revoked"
            ]
          },
          "404": {
            "description": "No such record for this campaign: `not_found`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_found"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "not_found"
            ]
          },
          "422": {
            "description": "The request is not valid: `campaign_mismatch`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "campaign_mismatch"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "campaign_mismatch"
            ]
          },
          "429": {
            "description": "Too many requests: `rate_limited`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "rate_limited"
            ]
          },
          "500": {
            "description": "Something went wrong on our side: `internal_error`",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/ErrorDetail"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "internal_error"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            },
            "x-utc-error-codes": [
              "internal_error"
            ]
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "campaignKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "The campaign key: `utc_test_…` or `utc_live_…`. Held on the brand's server, never in a browser."
      }
    },
    "parameters": {
      "id": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The id from the call that created the record.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "locale": {
        "name": "locale",
        "in": "query",
        "required": false,
        "description": "`en` or `sw`. Wins over `Accept-Language`.",
        "schema": {
          "type": "string",
          "enum": [
            "en",
            "sw"
          ]
        }
      },
      "IdempotencyKeyRequired": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "16 to 128 characters. Mint one per attempt in the browser when the consumer presses submit, and forward it unchanged on every retry of that attempt.",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 128
        },
        "example": "8b1f0c1e-5f0a-4c53-9a4e-2f6c8a1d7e90"
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "16 to 128 characters. Mint one per attempt in the browser when the consumer presses submit, and forward it unchanged on every retry of that attempt.",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 128
        },
        "example": "8b1f0c1e-5f0a-4c53-9a4e-2f6c8a1d7e90"
      },
      "XRequestId": {
        "name": "X-Request-Id",
        "in": "header",
        "required": false,
        "description": "Your own id for the request: 8 to 128 characters of letters, digits, `.`, `_`, `:` or `-`. It is written to our logs and the submission row and echoed back. Anything else is replaced with an id we mint.",
        "schema": {
          "type": "string"
        },
        "example": "req_4f9a1c2b7d3e"
      },
      "UTCCampaign": {
        "name": "UTC-Campaign",
        "in": "header",
        "required": false,
        "description": "With a campaign key the campaign comes from the key, so leave this out. If sent, it must equal the key's campaign or the call is 422 `campaign_mismatch`.",
        "schema": {
          "type": "string"
        },
        "example": "spier"
      },
      "UTCClientIP": {
        "name": "UTC-Client-IP",
        "in": "header",
        "required": false,
        "description": "The consumer's IP address as your server sees it. The per-address limits on these two calls then count per consumer; without it they count per brand server, which is stricter.",
        "schema": {
          "type": "string"
        },
        "example": "196.41.60.77"
      },
      "AcceptLanguage": {
        "name": "Accept-Language",
        "in": "header",
        "required": false,
        "description": "`sw` for Swahili; anything else is English. Sets the language of `message` and of `messages` on the campaign call. `?locale=en|sw` on the URL does the same and wins.",
        "schema": {
          "type": "string"
        },
        "example": "sw"
      }
    },
    "headers": {
      "XRequestId": {
        "description": "The request id: yours if it was usable, otherwise ours. Quote it when asking about a request. It is also `error.request_id`.",
        "schema": {
          "type": "string"
        },
        "example": "req_4f9a1c2b7d3e"
      },
      "IdempotentReplayed": {
        "description": "`true` when this `Idempotency-Key` and body were seen before and the stored submission is being returned. Absent otherwise.",
        "schema": {
          "type": "string"
        },
        "example": "true"
      },
      "RetryAfter": {
        "description": "Whole seconds to wait before sending the same request again.",
        "schema": {
          "type": "string"
        },
        "example": "12"
      }
    },
    "schemas": {
      "SubmissionRequest": {
        "type": "object",
        "properties": {
          "phone": {
            "description": "The consumer's Tanzanian mobile number as typed: `+255…`, `255…` or `0…`. Stored and returned as E.164.",
            "type": "string",
            "minLength": 9,
            "maxLength": 16
          },
          "code": {
            "description": "The crown code or the code we minted. Required whenever `capabilities.code_required` is true on the campaign.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "consent": {
            "description": "Must be `true`: the consumer ticked the consent box. Anything else is 422 `consent_required`.",
            "const": true
          },
          "region": {
            "description": "The consumer's region, if you collect it.",
            "type": "string",
            "maxLength": 80
          },
          "receipt_id": {
            "description": "The `id` from POST /api/v1/receipts. Required when `capabilities.receipt_required` is true.",
            "type": "string",
            "format": "uuid"
          },
          "claimed": {
            "description": "What the consumer says they bought. Recorded for comparison; it never decides anything.",
            "type": "object",
            "properties": {
              "items": {
                "description": "Product keys from the campaign call, with quantities.",
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "description": "A product `key` from `receipt.products` on the campaign call.",
                      "type": "string"
                    },
                    "qty": {
                      "description": "How many.",
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 99
                    }
                  },
                  "required": [
                    "key",
                    "qty"
                  ]
                },
                "maxItems": 40
              },
              "branch": {
                "description": "The branch the consumer chose.",
                "type": "string",
                "maxLength": 120
              }
            }
          }
        },
        "required": [
          "phone",
          "consent"
        ]
      },
      "Submission": {
        "type": "object",
        "properties": {
          "id": {
            "description": "The submission's id. Random; safe to show.",
            "type": "string",
            "format": "uuid"
          },
          "ref": {
            "description": "A short reference the consumer can quote.",
            "type": "string"
          },
          "campaign": {
            "description": "The campaign's slug.",
            "type": "string"
          },
          "livemode": {
            "description": "False when made with a test key.",
            "type": "boolean"
          },
          "state": {
            "description": "`pending` (still being checked), `accepted`, `rejected`, or `unknown` (the upstream did not answer; we are settling it).",
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "rejected",
              "unknown"
            ]
          },
          "review_status": {
            "description": "Receipt campaigns only: `verified`, `under_review` or `declined`. Null for code-only submissions and for ones refused before the receipt was read.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "verified",
                  "under_review",
                  "declined"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "display_status": {
            "description": "The one status to show a consumer. Computed by the server from `state` and `review_status`.",
            "type": "string",
            "enum": [
              "entered",
              "checking",
              "receipt_in_review",
              "not_accepted",
              "needs_attention"
            ]
          },
          "message_key": {
            "description": "Which message this is. Branch on this, never on `message`.",
            "type": "string",
            "enum": [
              "entered",
              "instant_win",
              "invalid",
              "code_required",
              "already_used",
              "daily_limit",
              "blocked",
              "checking",
              "receipt_received",
              "receipt_unreadable",
              "receipt_not_uploaded",
              "receipt_wrong_store",
              "receipt_outside_window",
              "receipt_no_qualifying_product",
              "receipt_already_used",
              "receipt_not_found",
              "receipt_time_mismatch",
              "entry_limit",
              "code_held",
              "campaign_not_started",
              "campaign_ended",
              "campaign_paused",
              "rate_limited",
              "service_unavailable",
              "file_too_large",
              "file_wrong_type"
            ]
          },
          "message": {
            "description": "The copy for `message_key` in the request's language, with the campaign's overrides. Show it as it is; it is also the fallback for a key your code does not know.",
            "type": "string"
          },
          "reasons": {
            "description": "Lower snake case reasons behind the outcome. Each is a message key or an ops-only reason. For logs, not for branching.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "entries": {
            "description": "Entries this submission earned. 0 until accepted.",
            "type": "integer",
            "minimum": 0
          },
          "instant_win": {
            "description": "The instant prize, in words, when there is one.",
            "type": [
              "string",
              "null"
            ]
          },
          "terminal": {
            "description": "True when nothing more will happen without a person acting: `accepted` or `rejected`.",
            "type": "boolean"
          },
          "poll_after_seconds": {
            "description": "Null when terminal. Otherwise how long to wait before calling GET /api/v1/submissions/{id}.",
            "type": [
              "integer",
              "null"
            ]
          },
          "replayed": {
            "description": "True when this is an earlier submission returned for the same campaign, code and phone, so a refresh never tells a consumer their own code is used.",
            "type": "boolean"
          },
          "receipt": {
            "description": "Present once TRA has returned the receipt's record; null until then and for code-only campaigns.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "date": {
                    "description": "The receipt's date on TRA's record.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "branch": {
                    "description": "The participating branch it was matched to.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "items_summary": {
                    "description": "The qualifying items, in words.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "tra_verified": {
                    "description": "True: this object is only present when TRA's record was found.",
                    "type": "boolean"
                  }
                },
                "required": [
                  "date",
                  "branch",
                  "items_summary",
                  "tra_verified"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "upstream": {
            "description": "Seebait's own code and text, for Seebait campaigns. Unstable: never branch on it.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "code": {
                    "description": "Seebait's `body.code`.",
                    "anyOf": [
                      {
                        "anyOf": [
                          {
                            "type": "number"
                          },
                          {
                            "type": "string"
                          }
                        ]
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "message": {
                    "description": "Seebait's message.",
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "code",
                  "message"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "description": "ISO 8601, UTC.",
            "type": "string"
          },
          "updated_at": {
            "description": "ISO 8601, UTC.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "ref",
          "campaign",
          "livemode",
          "state",
          "review_status",
          "display_status",
          "message_key",
          "message",
          "reasons",
          "entries",
          "instant_win",
          "terminal",
          "poll_after_seconds",
          "replayed",
          "receipt",
          "upstream",
          "created_at",
          "updated_at"
        ]
      },
      "LookupRequest": {
        "type": "object",
        "properties": {
          "phone": {
            "description": "The phone to look up: `+255…`, `255…` or `0…`.",
            "type": "string",
            "minLength": 9,
            "maxLength": 16
          }
        },
        "required": [
          "phone"
        ]
      },
      "LookupResponse": {
        "type": "object",
        "properties": {
          "submissions": {
            "description": "Newest first, at most 50. Empty for a phone we have not seen.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "description": "The submission's id.",
                  "type": "string",
                  "format": "uuid"
                },
                "ref": {
                  "description": "The reference the consumer was shown.",
                  "type": "string"
                },
                "date": {
                  "description": "The day only (`YYYY-MM-DD`) in the campaign's timezone.",
                  "type": "string"
                },
                "display_status": {
                  "description": "The status to show.",
                  "type": "string",
                  "enum": [
                    "entered",
                    "checking",
                    "receipt_in_review",
                    "not_accepted",
                    "needs_attention"
                  ]
                },
                "message": {
                  "description": "The message for that submission, in the request's language.",
                  "type": "string"
                },
                "entries": {
                  "description": "Entries that submission earned.",
                  "type": "integer"
                }
              },
              "required": [
                "id",
                "ref",
                "date",
                "display_status",
                "message",
                "entries"
              ]
            }
          },
          "total_entries": {
            "description": "The sum of entries over accepted submissions.",
            "type": "integer"
          },
          "timezone": {
            "description": "The campaign's timezone, e.g. `Africa/Dar_es_Salaam`.",
            "type": "string"
          }
        },
        "required": [
          "submissions",
          "total_entries",
          "timezone"
        ]
      },
      "CodeCheckRequest": {
        "type": "object",
        "properties": {
          "code": {
            "description": "The code from the QR or as typed.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "phone": {
            "description": "The consumer's phone, if known. Their own entered code then reads as `valid`.",
            "type": "string"
          }
        },
        "required": [
          "code"
        ]
      },
      "CodeCheckResponse": {
        "type": "object",
        "properties": {
          "precheck": {
            "description": "`valid`, `invalid`, `already_used`, or `unsupported` for a Seebait campaign.",
            "type": "string",
            "enum": [
              "valid",
              "invalid",
              "already_used",
              "unsupported"
            ]
          },
          "message_key": {
            "description": "`invalid`, `already_used` or `code_held` when the code cannot be used; null otherwise.",
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "entered",
                  "instant_win",
                  "invalid",
                  "code_required",
                  "already_used",
                  "daily_limit",
                  "blocked",
                  "checking",
                  "receipt_received",
                  "receipt_unreadable",
                  "receipt_not_uploaded",
                  "receipt_wrong_store",
                  "receipt_outside_window",
                  "receipt_no_qualifying_product",
                  "receipt_already_used",
                  "receipt_not_found",
                  "receipt_time_mismatch",
                  "entry_limit",
                  "code_held",
                  "campaign_not_started",
                  "campaign_ended",
                  "campaign_paused",
                  "rate_limited",
                  "service_unavailable",
                  "file_too_large",
                  "file_wrong_type"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "message": {
            "description": "The copy for `message_key`; null when it is null.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "precheck",
          "message_key",
          "message"
        ]
      },
      "ReceiptCreateRequest": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "type": {
                "description": "`photo` or `typed`.",
                "const": "photo"
              },
              "content_type": {
                "description": "The file's type after you have downsized it. The upload must carry exactly this `Content-Type`.",
                "type": "string",
                "enum": [
                  "image/jpeg",
                  "image/png",
                  "image/webp"
                ]
              },
              "size_bytes": {
                "description": "The file's size in bytes after downsizing. The upload must be exactly this long. Over 5 MB is 413 `file_too_large`.",
                "type": "integer",
                "exclusiveMinimum": 0
              },
              "qr_payload": {
                "description": "The text of the receipt's QR code, if the browser decoded it. Used only to look the receipt up with TRA.",
                "type": "string",
                "maxLength": 400
              }
            },
            "required": [
              "type",
              "content_type",
              "size_bytes"
            ]
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "description": "`photo` or `typed`.",
                "const": "typed"
              },
              "code": {
                "description": "The receipt verification code printed under the QR.",
                "type": "string",
                "minLength": 6,
                "maxLength": 40
              },
              "time": {
                "description": "The time printed on the receipt: `hh:mm` or `hh:mm:ss`.",
                "type": "string",
                "pattern": "^\\d{1,2}:\\d{2}(:\\d{2})?$"
              }
            },
            "required": [
              "type",
              "code",
              "time"
            ]
          }
        ]
      },
      "UploadTarget": {
        "type": "object",
        "properties": {
          "url": {
            "description": "Where the browser PUTs the file. Presigned; valid until `expires_at`.",
            "type": "string",
            "format": "uri"
          },
          "method": {
            "description": "Always `PUT`.",
            "const": "PUT"
          },
          "headers": {
            "description": "Send exactly these headers with the PUT. They are part of the signature.",
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "expires_at": {
            "description": "ISO 8601, UTC. After this, ask for a fresh URL.",
            "type": "string"
          },
          "max_bytes": {
            "description": "The size limit.",
            "type": "integer"
          },
          "accepted_types": {
            "description": "The file types accepted.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "url",
          "method",
          "headers",
          "expires_at",
          "max_bytes",
          "accepted_types"
        ]
      },
      "Receipt": {
        "type": "object",
        "properties": {
          "id": {
            "description": "Send this as `receipt_id` on the submission.",
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "description": "`photo` or `typed`.",
            "type": "string",
            "enum": [
              "photo",
              "typed"
            ]
          },
          "state": {
            "description": "`awaiting_upload`, `uploaded` (a typed receipt starts here) or `attached` (a submission uses it).",
            "type": "string",
            "enum": [
              "awaiting_upload",
              "uploaded",
              "attached"
            ]
          },
          "upload": {
            "description": "The upload target for a photo that has not arrived. Null for a typed receipt, once the file is there, and on GET.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/UploadTarget"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "description": "ISO 8601, UTC.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "type",
          "state",
          "upload",
          "created_at"
        ]
      },
      "CampaignInfo": {
        "type": "object",
        "properties": {
          "slug": {
            "description": "The campaign's slug.",
            "type": "string"
          },
          "name": {
            "description": "The campaign's name.",
            "type": "string"
          },
          "brand": {
            "description": "The brand's name.",
            "type": "string"
          },
          "livemode": {
            "description": "False when the key is a test key.",
            "type": "boolean"
          },
          "status": {
            "description": "`live`, `paused`, `not_started` or `ended`, from the pause switch and the window, now.",
            "type": "string",
            "enum": [
              "live",
              "paused",
              "not_started",
              "ended"
            ]
          },
          "window": {
            "description": "When entries are taken.",
            "type": "object",
            "properties": {
              "start": {
                "description": "ISO 8601, UTC. Null when there is no start.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "end": {
                "description": "ISO 8601, UTC. Null when there is no end.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "start",
              "end"
            ]
          },
          "timezone": {
            "description": "The timezone days and limits are counted in.",
            "type": "string"
          },
          "capabilities": {
            "description": "What this campaign needs from your page.",
            "type": "object",
            "properties": {
              "precheck": {
                "description": "True when POST /api/v1/codes/check can tell you about a code.",
                "type": "boolean"
              },
              "code_required": {
                "description": "True when every submission must carry `code`.",
                "type": "boolean"
              },
              "receipt_required": {
                "description": "True when every submission must carry `receipt_id`.",
                "type": "boolean"
              },
              "typed_receipt": {
                "description": "True when POST /api/v1/receipts accepts `type: \"typed\"`.",
                "type": "boolean"
              }
            },
            "required": [
              "precheck",
              "code_required",
              "receipt_required",
              "typed_receipt"
            ]
          },
          "limits": {
            "description": "Per-phone limits. Null means no limit.",
            "type": "object",
            "properties": {
              "phone_daily": {
                "description": "Submissions per phone per day.",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "phone_total": {
                "description": "Submissions per phone over the campaign.",
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "phone_daily",
              "phone_total"
            ]
          },
          "receipt": {
            "description": "Receipt campaigns only: what to offer the consumer. Null otherwise.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "max_bytes": {
                    "description": "The photo size limit.",
                    "type": "integer"
                  },
                  "accepted_types": {
                    "description": "The photo types accepted.",
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "ranges": {
                    "description": "Product ranges, for grouping.",
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "description": "The range's name.",
                          "type": "string"
                        },
                        "tag": {
                          "description": "A short tag line, if any.",
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "name",
                        "tag"
                      ]
                    }
                  },
                  "products": {
                    "description": "The participating products.",
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "description": "Send this as `claimed.items[].key`.",
                          "type": "string"
                        },
                        "range": {
                          "description": "The range it belongs to.",
                          "type": "string"
                        },
                        "label": {
                          "description": "A short label.",
                          "type": "string"
                        },
                        "name": {
                          "description": "The full product name.",
                          "type": "string"
                        },
                        "color": {
                          "description": "A colour for the product, if set.",
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "image": {
                          "description": "An image path, if set.",
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "key",
                        "range",
                        "label",
                        "name",
                        "color",
                        "image"
                      ]
                    }
                  },
                  "branches": {
                    "description": "The participating branches.",
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "description": "The branch's key.",
                          "type": "string"
                        },
                        "name": {
                          "description": "The branch's name.",
                          "type": "string"
                        },
                        "city": {
                          "description": "Its city.",
                          "type": "string"
                        },
                        "merchant": {
                          "description": "The merchant it belongs to.",
                          "type": "string"
                        }
                      },
                      "required": [
                        "key",
                        "name",
                        "city",
                        "merchant"
                      ]
                    }
                  },
                  "draw_period": {
                    "description": "How often the draw runs, in words, if set.",
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "max_bytes",
                  "accepted_types",
                  "ranges",
                  "products",
                  "branches",
                  "draw_period"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "messages": {
            "description": "Every message key with its copy in the request's language and this campaign's overrides.",
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "required": [
          "slug",
          "name",
          "brand",
          "livemode",
          "status",
          "window",
          "timezone",
          "capabilities",
          "limits",
          "receipt",
          "messages"
        ]
      },
      "ErrorDetail": {
        "type": "object",
        "required": [
          "code",
          "message",
          "fix",
          "docs_url",
          "request_id"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "key_missing",
              "key_malformed",
              "key_unknown",
              "key_revoked",
              "key_wrong_environment",
              "session_expired",
              "campaign_not_assigned",
              "campaign_mismatch",
              "not_found",
              "validation_failed",
              "phone_invalid",
              "consent_required",
              "idempotency_key_required",
              "idempotency_key_invalid",
              "idempotency_key_reused",
              "idempotency_in_progress",
              "receipt_required",
              "receipt_not_uploaded",
              "receipt_already_attached",
              "typed_receipt_unsupported",
              "file_too_large",
              "file_wrong_type",
              "sentinel_with_live_key",
              "rate_limited",
              "ledger_unavailable",
              "internal_error"
            ],
            "description": "Branch on this."
          },
          "message": {
            "type": "string",
            "description": "What went wrong, for a developer. Not consumer copy."
          },
          "field": {
            "type": "string",
            "description": "The request field at fault, when there is one."
          },
          "fix": {
            "type": "string",
            "description": "What to change."
          },
          "docs_url": {
            "type": "string",
            "description": "This error in the docs: `<APP_URL>/docs/errors#<code>`."
          },
          "request_id": {
            "type": "string",
            "description": "The request id, as in `X-Request-Id`."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorDetail"
          }
        }
      }
    }
  },
  "x-utc-headers": [
    {
      "name": "Authorization",
      "direction": "request",
      "on": "Every call",
      "description": "`Bearer <key>`. The campaign key, `utc_test_…` or `utc_live_…`. It selects the campaign and the environment. Sent from your server only.",
      "example": "Bearer $UTC_API_KEY"
    },
    {
      "name": "Idempotency-Key",
      "direction": "request",
      "on": "Required on POST /api/v1/submissions; accepted on POST /api/v1/receipts",
      "description": "16 to 128 characters. Mint one per attempt in the browser when the consumer presses submit, and forward it unchanged on every retry of that attempt.",
      "example": "8b1f0c1e-5f0a-4c53-9a4e-2f6c8a1d7e90"
    },
    {
      "name": "X-Request-Id",
      "direction": "request",
      "on": "Every call (optional)",
      "description": "Your own id for the request: 8 to 128 characters of letters, digits, `.`, `_`, `:` or `-`. It is written to our logs and the submission row and echoed back. Anything else is replaced with an id we mint.",
      "example": "req_4f9a1c2b7d3e"
    },
    {
      "name": "UTC-Campaign",
      "direction": "request",
      "on": "Every call (optional)",
      "description": "With a campaign key the campaign comes from the key, so leave this out. If sent, it must equal the key's campaign or the call is 422 `campaign_mismatch`.",
      "example": "spier"
    },
    {
      "name": "UTC-Client-IP",
      "direction": "request",
      "on": "POST /api/v1/submissions/lookup and POST /api/v1/codes/check",
      "description": "The consumer's IP address as your server sees it. The per-address limits on these two calls then count per consumer; without it they count per brand server, which is stricter.",
      "example": "196.41.60.77"
    },
    {
      "name": "Accept-Language",
      "direction": "request",
      "on": "Every call (optional)",
      "description": "`sw` for Swahili; anything else is English. Sets the language of `message` and of `messages` on the campaign call. `?locale=en|sw` on the URL does the same and wins.",
      "example": "sw"
    },
    {
      "name": "X-Request-Id",
      "direction": "response",
      "on": "Every reply",
      "description": "The request id: yours if it was usable, otherwise ours. Quote it when asking about a request. It is also `error.request_id`.",
      "example": "req_4f9a1c2b7d3e"
    },
    {
      "name": "Idempotent-Replayed",
      "direction": "response",
      "on": "200 from POST /api/v1/submissions",
      "description": "`true` when this `Idempotency-Key` and body were seen before and the stored submission is being returned. Absent otherwise.",
      "example": "true"
    },
    {
      "name": "Retry-After",
      "direction": "response",
      "on": "429 `rate_limited` and 409 `idempotency_in_progress`",
      "description": "Whole seconds to wait before sending the same request again.",
      "example": "12"
    }
  ],
  "x-utc-errors": {
    "key_missing": {
      "status": 401,
      "message": "No API key was sent.",
      "fix": "Send `Authorization: Bearer <key>` from your server."
    },
    "key_malformed": {
      "status": 401,
      "message": "The API key is not in a recognised format.",
      "fix": "Keys start with utc_test_ or utc_live_. Check for truncation or whitespace."
    },
    "key_unknown": {
      "status": 401,
      "message": "The API key is not recognised.",
      "fix": "Check the key was copied in full and belongs to this environment; ask ops for a new one if it was lost."
    },
    "key_revoked": {
      "status": 403,
      "message": "This API key has been revoked.",
      "fix": "Ask ops for a new key, or switch to the campaign's second live key."
    },
    "key_wrong_environment": {
      "status": 401,
      "message": "A test key was used where a live key is required, or the reverse.",
      "fix": "Use a utc_live_ key in production and a utc_test_ key everywhere else."
    },
    "session_expired": {
      "status": 401,
      "message": "The agent session has expired.",
      "fix": "Sign in again with the agent PIN; keep the typed submission and resend it with the same Idempotency-Key."
    },
    "campaign_not_assigned": {
      "status": 403,
      "message": "This agent is not assigned to that campaign.",
      "fix": "Send a `UTC-Campaign` header naming one of the campaigns from GET /api/v1/agent/me."
    },
    "campaign_mismatch": {
      "status": 422,
      "message": "The UTC-Campaign header does not match the key's campaign.",
      "fix": "Omit the header when using a campaign key."
    },
    "not_found": {
      "status": 404,
      "message": "No such record for this campaign.",
      "fix": "Check the id, and that it was created with a key for the same campaign and environment."
    },
    "validation_failed": {
      "status": 422,
      "message": "The request body is not valid.",
      "fix": "See `field` for the first problem and the reference for the expected shape."
    },
    "phone_invalid": {
      "status": 422,
      "message": "The phone number is not a valid Tanzanian mobile number.",
      "fix": "Send +255XXXXXXXXX, 255XXXXXXXXX or 0XXXXXXXXX."
    },
    "consent_required": {
      "status": 422,
      "message": "Consent must be true.",
      "fix": "Collect the consumer's consent and send `consent: true`."
    },
    "idempotency_key_required": {
      "status": 422,
      "message": "The Idempotency-Key header is required.",
      "fix": "Mint one key per attempt in the browser (16 to 128 characters) and forward it unchanged."
    },
    "idempotency_key_invalid": {
      "status": 422,
      "message": "The Idempotency-Key must be 16 to 128 characters.",
      "fix": "Use a UUID or another random string of that length."
    },
    "idempotency_key_reused": {
      "status": 422,
      "message": "This Idempotency-Key was already used with a different request body.",
      "fix": "Mint a new key for a new attempt; resend the same key only with the same body."
    },
    "idempotency_in_progress": {
      "status": 409,
      "message": "A request with this Idempotency-Key is still being processed.",
      "fix": "Wait for the Retry-After seconds, then resend the same request or poll the submission."
    },
    "receipt_required": {
      "status": 422,
      "message": "This campaign needs a receipt with every submission.",
      "fix": "Create one with POST /api/v1/receipts and send its id as `receipt_id`."
    },
    "receipt_not_uploaded": {
      "status": 422,
      "message": "The receipt photo has not been uploaded.",
      "fix": "PUT the file to the upload URL, or request a fresh one with POST /api/v1/receipts/:id/upload-url."
    },
    "receipt_already_attached": {
      "status": 422,
      "message": "This receipt already belongs to another submission.",
      "fix": "Create a new receipt for a new submission."
    },
    "typed_receipt_unsupported": {
      "status": 422,
      "message": "This campaign does not accept typed receipt codes.",
      "fix": "Check `capabilities.typed_receipt` on GET /api/v1/campaign."
    },
    "file_too_large": {
      "status": 413,
      "message": "The file is larger than the limit.",
      "fix": "Downsize the photo in the browser (longest side 2,000 px, JPEG) before uploading."
    },
    "file_wrong_type": {
      "status": 415,
      "message": "The file is not a JPEG, PNG or WebP image.",
      "fix": "Send one of the `accepted_types` from the upload response."
    },
    "sentinel_with_live_key": {
      "status": 422,
      "message": "A test sentinel was sent with a live key.",
      "fix": "Sentinel codes and receipts only work with utc_test_ keys."
    },
    "rate_limited": {
      "status": 429,
      "message": "Too many requests.",
      "fix": "Slow down and retry after the Retry-After seconds."
    },
    "ledger_unavailable": {
      "status": 503,
      "message": "The submission could not be recorded.",
      "fix": "Retry with the same Idempotency-Key."
    },
    "internal_error": {
      "status": 500,
      "message": "Something went wrong on our side.",
      "fix": "Retry with the same Idempotency-Key, and quote the request id if it keeps failing."
    }
  },
  "x-utc-reserved-errors": [
    "session_expired",
    "campaign_not_assigned",
    "ledger_unavailable"
  ],
  "x-utc-message-keys": {
    "entered": {
      "en": "You're in. Good luck!",
      "sw": "Umeingia. Kila la heri!"
    },
    "instant_win": {
      "en": "You've won an instant prize!",
      "sw": "Umeshinda zawadi ya papo hapo!"
    },
    "invalid": {
      "en": "That code isn't valid. Check it and try again.",
      "sw": "Namba hiyo si sahihi. Ikague kisha ujaribu tena."
    },
    "code_required": {
      "en": "Scan the code in store to enter.",
      "sw": "Skani namba iliyopo dukani ili kushiriki."
    },
    "already_used": {
      "en": "That code has already been used.",
      "sw": "Namba hiyo imeshatumika."
    },
    "daily_limit": {
      "en": "You've reached today's limit. Try again tomorrow.",
      "sw": "Umefikia kikomo cha leo. Jaribu tena kesho."
    },
    "blocked": {
      "en": "This number can't enter right now.",
      "sw": "Namba hii haiwezi kushiriki kwa sasa."
    },
    "checking": {
      "en": "We're checking your entry. You can close this page; we'll have the result shortly.",
      "sw": "Tunakagua ushiriki wako. Unaweza kufunga ukurasa huu; matokeo yatapatikana punde."
    },
    "receipt_received": {
      "en": "Receipt received. We're checking it with TRA. You can close this page and check your entries later.",
      "sw": "Risiti imepokelewa. Tunaikagua na TRA. Unaweza kufunga ukurasa huu na kuangalia ushiriki wako baadaye."
    },
    "receipt_unreadable": {
      "en": "We couldn't read the QR code on that photo. Type the receipt code and time instead, or we'll review it by hand.",
      "sw": "Hatukuweza kusoma QR kwenye picha hiyo. Andika namba ya risiti na muda, au tutaikagua wenyewe."
    },
    "receipt_not_uploaded": {
      "en": "The receipt photo didn't arrive. Please upload it again.",
      "sw": "Picha ya risiti haikufika. Tafadhali ipakie tena."
    },
    "receipt_wrong_store": {
      "en": "That receipt isn't from a participating store.",
      "sw": "Risiti hiyo haitoki kwenye duka linaloshiriki."
    },
    "receipt_outside_window": {
      "en": "That receipt is dated outside the promotion.",
      "sw": "Tarehe ya risiti hiyo iko nje ya kipindi cha promosheni."
    },
    "receipt_no_qualifying_product": {
      "en": "We couldn't find a participating product on that receipt.",
      "sw": "Hatukupata bidhaa inayoshiriki kwenye risiti hiyo."
    },
    "receipt_already_used": {
      "en": "That receipt has already been entered.",
      "sw": "Risiti hiyo imeshatumika kushiriki."
    },
    "receipt_not_found": {
      "en": "We couldn't find that receipt with TRA. Check the code and time, or upload a clearer photo.",
      "sw": "Hatukuipata risiti hiyo TRA. Kagua namba na muda, au pakia picha iliyo wazi zaidi."
    },
    "receipt_time_mismatch": {
      "en": "The time doesn't match TRA's record. Check the time printed on the receipt and try again.",
      "sw": "Muda haulingani na kumbukumbu za TRA. Kagua muda uliochapishwa kwenye risiti kisha ujaribu tena."
    },
    "entry_limit": {
      "en": "This number has reached the entry limit for this promotion.",
      "sw": "Namba hii imefikia kikomo cha ushiriki kwenye promosheni hii."
    },
    "code_held": {
      "en": "That code is attached to an entry that's still being checked.",
      "sw": "Namba hiyo inatumika kwenye ushiriki unaoendelea kukaguliwa."
    },
    "campaign_not_started": {
      "en": "The promotion hasn't started yet.",
      "sw": "Promosheni bado haijaanza."
    },
    "campaign_ended": {
      "en": "The promotion has ended.",
      "sw": "Promosheni imekwisha."
    },
    "campaign_paused": {
      "en": "Entries are paused for a moment. Please try again later.",
      "sw": "Ushiriki umesitishwa kwa muda. Tafadhali jaribu tena baadaye."
    },
    "rate_limited": {
      "en": "Too many tries. Please wait a little and try again.",
      "sw": "Umejaribu mara nyingi. Tafadhali subiri kidogo kisha ujaribu tena."
    },
    "service_unavailable": {
      "en": "We can't take entries right now. Please try again shortly.",
      "sw": "Hatuwezi kupokea ushiriki kwa sasa. Tafadhali jaribu tena punde."
    },
    "file_too_large": {
      "en": "That photo is too large. Please take it again.",
      "sw": "Picha hiyo ni kubwa mno. Tafadhali piga tena."
    },
    "file_wrong_type": {
      "en": "That file isn't a photo we can read. Use JPEG, PNG or WebP.",
      "sw": "Faili hilo si picha tunayoweza kusoma. Tumia JPEG, PNG au WebP."
    }
  },
  "x-utc-display-statuses": {
    "entered": {
      "en": "Entered",
      "sw": "Umeingia"
    },
    "checking": {
      "en": "Checking",
      "sw": "Inakaguliwa"
    },
    "receipt_in_review": {
      "en": "Receipt in review",
      "sw": "Risiti inakaguliwa"
    },
    "not_accepted": {
      "en": "Not accepted",
      "sw": "Haijakubaliwa"
    },
    "needs_attention": {
      "en": "Needs attention",
      "sw": "Inahitaji kushughulikiwa"
    }
  },
  "x-utc-transitions": [
    {
      "from": "pending",
      "to": "accepted",
      "by": "upstream",
      "when": "Seebait accepted the code"
    },
    {
      "from": "pending",
      "to": "rejected",
      "by": "upstream",
      "when": "Seebait rejected the code"
    },
    {
      "from": "pending",
      "to": "unknown",
      "by": "upstream",
      "when": "Seebait timed out, failed, or answered with something unrecognised"
    },
    {
      "from": "pending",
      "to": "unknown",
      "by": "sweeper",
      "when": "A pending Seebait row was never sent and the single send did not settle"
    },
    {
      "from": "pending",
      "to": "accepted",
      "by": "sweeper",
      "when": "A pending Seebait row with no upstream_sent_at was sent once and accepted"
    },
    {
      "from": "pending",
      "to": "rejected",
      "by": "sweeper",
      "when": "A pending Seebait row with no upstream_sent_at was sent once and rejected"
    },
    {
      "from": "unknown",
      "to": "accepted",
      "by": "sweeper",
      "when": "The one resubmission was accepted, or answered already-used with no other accepted submission for the code"
    },
    {
      "from": "unknown",
      "to": "rejected",
      "by": "sweeper",
      "when": "The one resubmission was rejected"
    },
    {
      "from": "unknown",
      "to": "accepted",
      "by": "ops",
      "when": "Settled by hand from the attention queue"
    },
    {
      "from": "unknown",
      "to": "rejected",
      "by": "ops",
      "when": "Settled by hand from the attention queue"
    },
    {
      "from": "pending",
      "to": "accepted",
      "by": "submit",
      "when": "Native code redeemed; or receipt verified by TRA on the interactive attempt"
    },
    {
      "from": "pending",
      "to": "rejected",
      "by": "submit",
      "when": "Native code invalid or used; or a receipt rule failed on TRA's record"
    },
    {
      "from": "pending",
      "to": "accepted",
      "by": "recheck",
      "when": "The re-check job got TRA's record and every rule passed"
    },
    {
      "from": "pending",
      "to": "rejected",
      "by": "recheck",
      "when": "The re-check job got TRA's record and a rule failed"
    },
    {
      "from": "pending",
      "to": "accepted",
      "by": "review",
      "when": "A reviewer approved a receipt that TRA has returned"
    },
    {
      "from": "pending",
      "to": "rejected",
      "by": "review",
      "when": "A reviewer declined the receipt; the in-store code is released"
    }
  ],
  "x-utc-outcomes": [
    {
      "condition": "The code is good and the campaign needs nothing else",
      "kind": "consumer",
      "http_status": 200,
      "state": "accepted",
      "review_status": null,
      "message_key": "entered",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The code is good and carries an instant prize",
      "kind": "consumer",
      "http_status": 200,
      "state": "accepted",
      "review_status": null,
      "message_key": "instant_win",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "`instant_win` names the prize."
    },
    {
      "condition": "The code is good and TRA's record of the receipt passes every rule",
      "kind": "consumer",
      "http_status": 200,
      "state": "accepted",
      "review_status": "verified",
      "message_key": "entered",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "`entries` is the number of qualifying items on TRA's record."
    },
    {
      "condition": "The code is being checked and has not settled yet",
      "kind": "consumer",
      "http_status": 200,
      "state": "pending",
      "review_status": null,
      "message_key": "checking",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "Poll GET /api/v1/submissions/{id} as `poll_after_seconds` says. The sentinel `TEST-0-SETTLES` does this."
    },
    {
      "condition": "The upstream did not answer, or answered with something unrecognised",
      "kind": "consumer",
      "http_status": 200,
      "state": "unknown",
      "review_status": null,
      "message_key": "checking",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "Shown to the consumer as \"Checking\". Poll; do not submit again: a retry returns this same row."
    },
    {
      "condition": "The receipt is recorded and waiting for TRA, or for a person",
      "kind": "consumer",
      "http_status": 200,
      "state": "pending",
      "review_status": "under_review",
      "message_key": "receipt_received",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "TRA 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."
    },
    {
      "condition": "The receipt submission is recorded and its one TRA lookup is running",
      "kind": "consumer",
      "http_status": 200,
      "state": "pending",
      "review_status": "under_review",
      "message_key": "checking",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "Seen 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."
    },
    {
      "condition": "The QR code on the receipt photo could not be read",
      "kind": "consumer",
      "http_status": 200,
      "state": "pending",
      "review_status": "under_review",
      "message_key": "receipt_unreadable",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "Offer the typed path (a new receipt and a new submission); otherwise a person reviews the photo."
    },
    {
      "condition": "The code is not recognised",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "invalid",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "Also a code from a live batch sent with a test key, and the reverse."
    },
    {
      "condition": "The code was already used by an entry that counted",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "already_used",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The code is attached to another entry that is still being checked",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "code_held",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "The code is released if that entry ends rejected."
    },
    {
      "condition": "The phone has reached the campaign's daily limit",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "daily_limit",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "Tomorrow, in the campaign's timezone."
    },
    {
      "condition": "The phone has reached the campaign's total limit",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "entry_limit",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The campaign needs an in-store code and none was sent",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "code_required",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "Native campaigns. With the code from the in-store QR."
    },
    {
      "condition": "The phone is blocked from entering",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "blocked",
      "error_code": null,
      "row_written": true,
      "retryable": false,
      "note": "Seebait 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`."
    },
    {
      "condition": "The campaign's window has not opened",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "campaign_not_started",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "Check `status` on GET /api/v1/campaign before showing the form."
    },
    {
      "condition": "The campaign's window has closed",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "campaign_ended",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The campaign is paused",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": null,
      "message_key": "campaign_paused",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "Ops paused it; entries resume when it is set live again."
    },
    {
      "condition": "The receipt is not from a participating store",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "receipt_wrong_store",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The receipt is dated outside the promotion",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "receipt_outside_window",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "No participating product is on the receipt",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "receipt_no_qualifying_product",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The receipt has already been entered",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "receipt_already_used",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "TRA has no receipt with that code",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "receipt_not_found",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "With a corrected code, or a clearer photo, as a new receipt and a new submission."
    },
    {
      "condition": "The typed time does not match TRA's record",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "receipt_time_mismatch",
      "error_code": null,
      "row_written": true,
      "retryable": true,
      "note": "With the time exactly as printed, as a new receipt and a new submission."
    },
    {
      "condition": "The phone's entries from receipts have reached the campaign's cap",
      "kind": "consumer",
      "http_status": 200,
      "state": "rejected",
      "review_status": "declined",
      "message_key": "entry_limit",
      "error_code": null,
      "row_written": true,
      "retryable": false
    },
    {
      "condition": "The same Idempotency-Key and the same body were sent before",
      "kind": "consumer",
      "http_status": 200,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "The stored submission comes back as it is now, with the header `Idempotent-Replayed: true`."
    },
    {
      "condition": "A new Idempotency-Key, but the same campaign, code and phone as an earlier submission that is pending, unknown or accepted",
      "kind": "consumer",
      "http_status": 200,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "The earlier submission comes back with `replayed: true`. After a rejected one, a new submission is made."
    },
    {
      "condition": "No `Authorization` header was sent",
      "kind": "caller",
      "http_status": 401,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "key_missing",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The header is not `Bearer <key>`, or the key is not in the `utc_test_…` / `utc_live_…` format",
      "kind": "caller",
      "http_status": 401,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "key_malformed",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The key is well formed but is not one of ours",
      "kind": "caller",
      "http_status": 401,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "key_unknown",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The key has been revoked",
      "kind": "caller",
      "http_status": 403,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "key_revoked",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "A live key was used on a deployment that is not production",
      "kind": "caller",
      "http_status": 401,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "key_wrong_environment",
      "row_written": false,
      "retryable": false,
      "note": "Test keys work on every deployment, production included."
    },
    {
      "condition": "A `UTC-Campaign` header names a campaign other than the key's",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "campaign_mismatch",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "A BA agent's session has expired",
      "kind": "caller",
      "http_status": 401,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "session_expired",
      "row_written": false,
      "retryable": false,
      "note": "BA sessions arrive with the BA portal cutover.",
      "available": false
    },
    {
      "condition": "A BA agent named a campaign they are not assigned to",
      "kind": "caller",
      "http_status": 403,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "campaign_not_assigned",
      "row_written": false,
      "retryable": false,
      "note": "BA sessions arrive with the BA portal cutover.",
      "available": false
    },
    {
      "condition": "The body is not JSON, or a field is missing or has the wrong shape",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "validation_failed",
      "row_written": false,
      "retryable": false,
      "note": "`error.field` names the first problem. Also: no `code` where the campaign needs one; a receipt for a campaign that takes none."
    },
    {
      "condition": "The phone is not a Tanzanian mobile number",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "phone_invalid",
      "row_written": false,
      "retryable": false,
      "note": "Validate in the form first; this is the caller's request, so no row is written."
    },
    {
      "condition": "`consent` is missing or not `true`",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "consent_required",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "POST /api/v1/submissions was sent without an `Idempotency-Key`",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "idempotency_key_required",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The `Idempotency-Key` is shorter than 16 or longer than 128 characters",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "idempotency_key_invalid",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The `Idempotency-Key` was used before with a different body",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "idempotency_key_reused",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "A receipt submission with this `Idempotency-Key` is still being processed",
      "kind": "caller",
      "http_status": 409,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "idempotency_in_progress",
      "row_written": false,
      "retryable": true,
      "note": "Wait `Retry-After` seconds. Past the time budget the stored pending submission is returned instead."
    },
    {
      "condition": "A `TEST-0-…` code or `TEST0…` receipt was sent with a live key",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "sentinel_with_live_key",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The id is not a uuid, or names nothing in this key's campaign and environment",
      "kind": "caller",
      "http_status": 404,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "not_found",
      "row_written": false,
      "retryable": false,
      "note": "Also a `receipt_id` that does not exist, and a key whose campaign is not in the database."
    },
    {
      "condition": "The campaign needs a receipt and the submission has no `receipt_id`",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "receipt_required",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The receipt's photo is not in storage",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "receipt_not_uploaded",
      "row_written": false,
      "retryable": true,
      "note": "After the PUT succeeds, with the same Idempotency-Key."
    },
    {
      "condition": "The receipt already belongs to a submission",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "receipt_already_attached",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "A typed receipt was sent to a campaign that takes no receipts",
      "kind": "caller",
      "http_status": 422,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "typed_receipt_unsupported",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "`size_bytes` is over 5 MB, or the stored file is",
      "kind": "caller",
      "http_status": 413,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "file_too_large",
      "row_written": false,
      "retryable": false
    },
    {
      "condition": "The stored file is not a JPEG, PNG or WebP",
      "kind": "caller",
      "http_status": 415,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "file_wrong_type",
      "row_written": false,
      "retryable": false,
      "note": "A wrong `content_type` on POST /api/v1/receipts is 422 `validation_failed`; this is the file itself, found at submission."
    },
    {
      "condition": "The key is over its requests-per-minute ceiling, or the lookup or code-check limit was reached",
      "kind": "caller",
      "http_status": 429,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "rate_limited",
      "row_written": false,
      "retryable": true,
      "note": "After `Retry-After` seconds."
    },
    {
      "condition": "Something failed on our side",
      "kind": "caller",
      "http_status": 500,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "internal_error",
      "row_written": false,
      "retryable": true,
      "note": "With the same Idempotency-Key. Quote the request id if it keeps failing."
    },
    {
      "condition": "The submission could not be recorded",
      "kind": "caller",
      "http_status": 503,
      "state": null,
      "review_status": null,
      "message_key": null,
      "error_code": "ledger_unavailable",
      "row_written": false,
      "retryable": true,
      "note": "Reserved: no call answers with it today.",
      "available": false
    },
    {
      "condition": "Show when the API answers 422 `receipt_not_uploaded`",
      "kind": "frontend",
      "http_status": null,
      "state": null,
      "review_status": null,
      "message_key": "receipt_not_uploaded",
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "The copy is in `messages` on GET /api/v1/campaign."
    },
    {
      "condition": "Show when the chosen photo is over the limit, or the API answers 413",
      "kind": "frontend",
      "http_status": null,
      "state": null,
      "review_status": null,
      "message_key": "file_too_large",
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "Check the size in the browser before asking for an upload URL."
    },
    {
      "condition": "Show when the chosen file is not a JPEG, PNG or WebP, or the API answers 415",
      "kind": "frontend",
      "http_status": null,
      "state": null,
      "review_status": null,
      "message_key": "file_wrong_type",
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "Check the type in the browser first."
    },
    {
      "condition": "Show when the API answers 429",
      "kind": "frontend",
      "http_status": null,
      "state": null,
      "review_status": null,
      "message_key": "rate_limited",
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "Wait `Retry-After` seconds before letting the consumer try again."
    },
    {
      "condition": "Show when the API cannot be reached or answers 5xx after the client's retries",
      "kind": "frontend",
      "http_status": null,
      "state": null,
      "review_status": null,
      "message_key": "service_unavailable",
      "error_code": null,
      "row_written": false,
      "retryable": true,
      "note": "Keep the form's values and the Idempotency-Key so the consumer can press submit again."
    }
  ],
  "x-utc-sentinels": {
    "codes": [
      {
        "code": "TEST-0-ENTERED",
        "state": "accepted",
        "message_key": "entered",
        "entries": 1,
        "note": "Accepted"
      },
      {
        "code": "TEST-0-INSTANT-WIN",
        "state": "accepted",
        "message_key": "instant_win",
        "entries": 1,
        "instant_win": "TZS 2,000 airtime",
        "note": "Accepted with an instant prize"
      },
      {
        "code": "TEST-0-INVALID",
        "state": "rejected",
        "message_key": "invalid",
        "entries": 0,
        "note": "Code not recognised"
      },
      {
        "code": "TEST-0-ALREADY-USED",
        "state": "rejected",
        "message_key": "already_used",
        "entries": 0,
        "note": "Code used by someone else"
      },
      {
        "code": "TEST-0-DAILY-LIMIT",
        "state": "rejected",
        "message_key": "daily_limit",
        "entries": 0,
        "note": "Phone reached the daily limit"
      },
      {
        "code": "TEST-0-BLOCKED",
        "state": "rejected",
        "message_key": "blocked",
        "entries": 0,
        "note": "Phone blocked"
      },
      {
        "code": "TEST-0-UNKNOWN",
        "state": "unknown",
        "message_key": "checking",
        "entries": 0,
        "note": "Upstream timed out; stays unknown"
      },
      {
        "code": "TEST-0-SETTLES",
        "state": "pending",
        "message_key": "checking",
        "entries": 0,
        "settles_to": {
          "state": "accepted",
          "message_key": "entered",
          "entries": 1
        },
        "settle_after_seconds": 10,
        "note": "Pending, then accepted after 10 seconds: use it to test polling"
      }
    ],
    "receipts": [
      {
        "tra_code": "TEST0VERIFIED1",
        "state": "accepted",
        "review_status": "verified",
        "message_key": "entered",
        "entries": 1,
        "note": "TRA returns one qualifying bottle"
      },
      {
        "tra_code": "TEST0VERIFIED3",
        "state": "accepted",
        "review_status": "verified",
        "message_key": "entered",
        "entries": 3,
        "note": "TRA returns three qualifying bottles"
      },
      {
        "tra_code": "TEST0PENDING",
        "state": "pending",
        "review_status": "under_review",
        "message_key": "receipt_received",
        "entries": 0,
        "note": "TRA did not answer in time; the re-check job finishes it"
      },
      {
        "tra_code": "TEST0UNREADABLE",
        "state": "pending",
        "review_status": "under_review",
        "message_key": "receipt_unreadable",
        "entries": 0,
        "note": "QR could not be read from the photo"
      },
      {
        "tra_code": "TEST0WRONGSTORE",
        "state": "rejected",
        "review_status": "declined",
        "message_key": "receipt_wrong_store",
        "entries": 0,
        "note": "Receipt from another merchant"
      },
      {
        "tra_code": "TEST0OUTSIDE",
        "state": "rejected",
        "review_status": "declined",
        "message_key": "receipt_outside_window",
        "entries": 0,
        "note": "Receipt dated outside the window"
      },
      {
        "tra_code": "TEST0NOPRODUCT",
        "state": "rejected",
        "review_status": "declined",
        "message_key": "receipt_no_qualifying_product",
        "entries": 0,
        "note": "No participating product on the receipt"
      },
      {
        "tra_code": "TEST0DUPLICATE",
        "state": "rejected",
        "review_status": "declined",
        "message_key": "receipt_already_used",
        "entries": 0,
        "note": "Receipt already entered"
      },
      {
        "tra_code": "TEST0REVIEW",
        "state": "pending",
        "review_status": "under_review",
        "message_key": "receipt_received",
        "entries": 0,
        "note": "A brand line that matches no product: held for a person"
      }
    ],
    "test_campaigns": {
      "native": "test-native",
      "receipt": "test-receipt",
      "seebait": "test-seebait",
      "notStarted": "test-not-started",
      "ended": "test-ended",
      "paused": "test-paused"
    }
  }
}
