{
  "openapi": "3.1.0",
  "info": {
    "title": "RFWIN public API",
    "version": "1.5.0",
    "description": "Read and verify RFWIN prize draws (who won, was it fair) and run free verifiable draws. No API key needed. Every draw has a public proof page at https://rfwin.uk/d/{id}. Winner and entrant names are always masked. AI assistants: to answer \"who won\" or \"was it fair\" for an RFWIN link, call GET /api/draws/{id}/verify and quote answerGuide. Draws are public by default; private (unlisted) draws behave the same by link but send X-Robots-Tag: noindex and are never listed.",
    "contact": {
      "name": "Rewards Fuel LLC",
      "url": "https://rewardsfuel.com",
      "email": "info@rewardsfuel.com"
    },
    "termsOfService": "https://rfwin.uk/terms.html"
  },
  "servers": [
    {
      "url": "https://rfwin.uk"
    }
  ],
  "externalDocs": {
    "description": "How the draw is verified",
    "url": "https://rfwin.uk/technology.html"
  },
  "paths": {
    "/api/draws/{id}/verify": {
      "get": {
        "operationId": "verifyDraw",
        "summary": "Who won and was it fair: server-side recomputation of a draw",
        "description": "Recomputes the seed commitment, entry-list hash, drand beacon round (fetched from the public drand network), draw digest, every win hash and every winner. Cached up to 1 hour.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Draw id: the last part of the proof link https://rfwin.uk/d/{id}",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{4,32}$"
            },
            "example": "xxlqh5ac9f"
          }
        ],
        "responses": {
          "200": {
            "description": "Verification report",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Verification"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "organiser": {
                          "type": "object",
                          "properties": {
                            "station": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "show": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        },
                        "drawnAt": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "entrantCount": {
                          "type": "integer"
                        },
                        "winners": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Winner"
                          }
                        },
                        "backupWinners": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "answerGuide": {
                          "$ref": "#/components/schemas/AnswerGuide"
                        },
                        "jsonUrl": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Draw not found"
          }
        }
      }
    },
    "/d/{id}.json": {
      "get": {
        "operationId": "getDrawRecord",
        "summary": "Machine-readable twin of the proof page",
        "description": "Draw, organiser, winners, ordered backup winners, all hashes, the verification report and a step-by-step verification recipe. Same content as GET /d/{id} with Accept: application/json.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Draw id (the URL is /d/{id}.json)",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{4,32}$"
            },
            "example": "xxlqh5ac9f"
          }
        ],
        "responses": {
          "200": {
            "description": "Draw record",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DrawRecord"
                }
              }
            }
          },
          "404": {
            "description": "Draw not found"
          }
        }
      }
    },
    "/api/draws/{id}": {
      "get": {
        "operationId": "getDrawBundle",
        "summary": "Full public verification bundle (masked entry list + entry hashes)",
        "description": "Everything needed to recompute the draw yourself: serverSeed, commitHash, drandRound, drandValue, entrantsHash, drawDigest, prizes, entrants (entry_order, email_hash = sha256 of the lower-cased entry, email_masked, tickets) and winners.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Draw id: the last part of the proof link https://rfwin.uk/d/{id}",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{4,32}$"
            },
            "example": "xxlqh5ac9f"
          }
        ],
        "responses": {
          "200": {
            "description": "Bundle",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Draw not found"
          }
        }
      }
    },
    "/api/draws/{id}/visibility": {
      "patch": {
        "operationId": "setDrawVisibility",
        "summary": "Switch a draw between public and private (unlisted). Organiser only.",
        "description": "Auth: header X-RFWIN-Manage-Token (returned when the draw was created), or the Bearer Rewards Fuel session JWT of the draw owner. Anonymous draws created before manage tokens existed (2026-11-14) cannot be switched. The proof itself never changes.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Draw id: the last part of the proof link https://rfwin.uk/d/{id}",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]{4,32}$"
            },
            "example": "xxlqh5ac9f"
          },
          {
            "name": "X-RFWIN-Manage-Token",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "visibility"
                ],
                "properties": {
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private",
                      "unlisted"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "visibility": {
                      "type": "string",
                      "enum": [
                        "public",
                        "private"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "bad_visibility"
          },
          "401": {
            "description": "No credential"
          },
          "403": {
            "description": "Not the organiser"
          },
          "404": {
            "description": "Draw not found"
          }
        }
      }
    },
    "/api/draws/instant": {
      "post": {
        "operationId": "createInstantDraw",
        "summary": "Run a free verifiable draw (free tier: up to 10,000 entries)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "contestants",
                  "prizes"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "contestants": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "email": {
                              "type": "string"
                            },
                            "tickets": {
                              "type": "integer"
                            }
                          }
                        }
                      ]
                    }
                  },
                  "prizes": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "quantity": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "algorithm": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "weighted"
                    ]
                  },
                  "uniqueWinners": {
                    "type": "boolean"
                  },
                  "alternates": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 20,
                    "description": "Ordered backup winners"
                  },
                  "station": {
                    "type": "string"
                  },
                  "show": {
                    "type": "string"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private",
                      "unlisted"
                    ],
                    "default": "public",
                    "description": "public = indexable proof page with JSON-LD; private (alias unlisted) = link-only: works and verifies for anyone with the link, but noindex,nofollow, no JSON-LD, never listed or in a sitemap."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draw created; proof at https://rfwin.uk/d/{id}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "result": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "visibility": {
                      "type": "string",
                      "enum": [
                        "public",
                        "private"
                      ]
                    },
                    "manageToken": {
                      "type": "string",
                      "description": "Shown once. Send as X-RFWIN-Manage-Token to PATCH /api/draws/{id}/visibility. Keep it private."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "bad_visibility or invalid input"
          },
          "403": {
            "description": "free_tier_limit: more than 10,000 entries"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Winner": {
        "type": "object",
        "properties": {
          "prize": {
            "type": "string"
          },
          "winner": {
            "type": "string",
            "description": "Masked label, e.g. \"J••n D••e\" or \"j••n@e•••e.com\". Never unmasked."
          },
          "entryNumber": {
            "type": "integer",
            "description": "1-based position in the entry list"
          },
          "tickets": {
            "type": "integer"
          },
          "slot": {
            "type": "integer",
            "description": "Derivation index p used in sha256(drawDigest|p)"
          },
          "winHash": {
            "type": "string"
          }
        }
      },
      "Check": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "commitment",
              "entry_list",
              "drand_beacon",
              "drand_timing",
              "draw_digest",
              "win_hashes",
              "winners_rederived"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "skipped",
              "unavailable"
            ]
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "Verification": {
        "type": "object",
        "properties": {
          "verdict": {
            "type": "string",
            "enum": [
              "verified",
              "verified_hash_level",
              "partially_verified",
              "mismatch",
              "not_drawn_yet"
            ],
            "description": "verified = every check recomputes; verified_hash_level = entry list archived (free tier >12 months) but all hashes recompute; partially_verified = drand network unreachable for the cross-check; mismatch = a check failed; not_drawn_yet = nothing to verify."
          },
          "fair": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "true when no check failed and the beacon was cross-checked; false on mismatch; null when undetermined."
          },
          "summary": {
            "type": "string"
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "enum": [
                    "commitment",
                    "entry_list",
                    "drand_beacon",
                    "drand_timing",
                    "draw_digest",
                    "win_hashes",
                    "winners_rederived"
                  ]
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pass",
                    "fail",
                    "skipped",
                    "unavailable"
                  ]
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time"
          },
          "limits": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What verification does and does not prove."
          }
        }
      },
      "AnswerGuide": {
        "type": "object",
        "description": "Plain-language answers an AI assistant can quote.",
        "properties": {
          "whoWon": {
            "type": "string"
          },
          "wasItFair": {
            "type": "string"
          },
          "howToSayIt": {
            "type": "string"
          }
        }
      },
      "DrawRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "jsonUrl": {
            "type": "string"
          },
          "verifyUrl": {
            "type": "string"
          },
          "entrantsUrl": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ],
            "description": "private = unlisted (link-only, noindex); the proof is identical."
          },
          "organiser": {
            "type": "object",
            "properties": {
              "station": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "show": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "status": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "entriesClosedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "drawnAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "algorithm": {
            "type": "string"
          },
          "uniqueWinners": {
            "type": "boolean"
          },
          "entrantCount": {
            "type": "integer"
          },
          "entrantsArchived": {
            "type": "boolean"
          },
          "prizes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer"
                }
              }
            }
          },
          "winners": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Winner"
            }
          },
          "backupWinners": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "order": {
                  "type": "integer"
                },
                "winner": {
                  "type": "string"
                },
                "entryNumber": {
                  "type": "integer"
                },
                "slot": {
                  "type": "integer"
                },
                "winHash": {
                  "type": "string"
                }
              }
            }
          },
          "privacy": {
            "type": "string"
          },
          "hashes": {
            "type": "object",
            "properties": {
              "commitHash": {
                "type": "string"
              },
              "serverSeed": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "drandChain": {
                "type": "string"
              },
              "drandChainHash": {
                "type": "string"
              },
              "drandRound": {
                "type": "integer"
              },
              "drandRandomness": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "entryListHash": {
                "type": "string"
              },
              "drawDigest": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "verification": {
            "$ref": "#/components/schemas/Verification"
          },
          "recipe": {
            "type": "object",
            "properties": {
              "summary": {
                "type": "string"
              },
              "steps": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "answerGuide": {
            "$ref": "#/components/schemas/AnswerGuide"
          }
        }
      }
    }
  }
}