{
  "openapi": "3.1.0",
  "info": {
    "title": "HaterPicks API (HaterAPI)",
    "version": "1.0.0",
    "summary": "Australian sports odds across every major bookmaker, as JSON.",
    "description": "The HaterPicks API returns live Australian sports-betting odds, merged and de-duplicated across Australian bookmakers. The Free plan covers game lines (head-to-head, totals and lines/spreads); Starter adds player props; Pro and Business add fair prices with edge %, arbitrage and same-game multi quotes. Plans: Free $0 AUD/month, 1,000 calls; Starter $29 AUD/month, 50,000 calls; Pro $79 AUD/month, 250,000 calls; Business $249 AUD/month, 1,000,000 calls. Every keyed response carries X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset headers for the monthly allowance, and RateLimit-Policy / RateLimit (IETF draft) plus X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for the per-minute limit; a 429 adds Retry-After. Versioning: the version is in the path (/api/v1) and v1 only ever adds fields, parameters and endpoints. A breaking change ships as /api/v2; the old version then sends Deprecation and Sunset headers and keeps answering for at least 6 months. Policy: https://www.haterpicks.app/api#versioning. Responses are gzipped when the client sends Accept-Encoding: gzip (use curl --compressed). HaterPicks is an analytics service, not a bookmaker: the API never places bets. Get a key at https://www.haterpicks.app/developer. Odds are decimal. Times are ISO 8601 UTC.",
    "contact": {
      "name": "HaterPicks Pty Ltd",
      "email": "contact@haterpicks.app",
      "url": "https://www.haterpicks.app/api"
    },
    "termsOfService": "https://www.haterpicks.app/terms"
  },
  "externalDocs": {
    "description": "HaterPicks API documentation",
    "url": "https://www.haterpicks.app/api"
  },
  "servers": [
    {
      "url": "https://www.haterpicks.app",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "meta",
      "description": "About the API itself. No key needed."
    },
    {
      "name": "odds",
      "description": "Odds feeds. Need an API key."
    },
    {
      "name": "access",
      "description": "Requesting a key. No key needed."
    }
  ],
  "security": [
    {
      "bearerKey": []
    },
    {
      "headerKey": []
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "Describe the API",
        "description": "Unauthenticated index of the API: its version, its endpoints, which tier each needs, and where the documentation and this specification live. Use it to check the API is up before calling a keyed feed.",
        "tags": [
          "meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The API index.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "429": {
            "description": "More than 120 requests a minute from one IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          }
        }
      }
    },
    "/api/v1/odds": {
      "get": {
        "operationId": "listOdds",
        "summary": "List live odds across Australian bookmakers",
        "description": "Every live market HaterPicks tracks, one row per selection and line, with each bookmaker's current decimal price. Stale prices are dropped. On the free tier the rows are game lines only (`filtered: \"game-lines-only\"`); Starter and above add player props. Pass `sport` for one sport.",
        "tags": [
          "odds"
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "required": false,
            "description": "Only rows for this sport: the rows' `sport` value, any case (AFL, NRL, NBL, NBA, NFL, SOCCER, RACING…). An off-season sport returns an empty list.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z][A-Za-z0-9_]{1,23}$"
            },
            "example": "AFL"
          }
        ],
        "responses": {
          "200": {
            "description": "Live odds rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "version",
                    "feed",
                    "generated_at",
                    "data"
                  ],
                  "properties": {
                    "version": {
                      "type": "string",
                      "const": "v1"
                    },
                    "feed": {
                      "type": "string",
                      "description": "Which feed produced this response."
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of items in `data`, on list feeds."
                    },
                    "filtered": {
                      "type": "string",
                      "description": "Set when the caller's tier trimmed the payload. The free tier returns `game-lines-only`."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OddsRow"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's plan does not include this feed (`upgrade_required: true`, `required_tier` names the plan that does).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-minute limit exceeded (retry after the `Retry-After` header), or the monthly allowance is used up (`quota_exceeded: true`, `resets_at` is when it refills: 00:00 UTC on the 1st).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "502": {
            "description": "The upstream feed was unavailable. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ev": {
      "get": {
        "operationId": "listEvOpportunities",
        "summary": "List +EV player-prop opportunities (Pro plan)",
        "description": "Player-prop prices that sit above HaterPicks' fair probability. `model_and_market` are backed by both the projection model and the de-vigged market; `market_only` by the market alone. Requires the Pro plan.",
        "tags": [
          "odds"
        ],
        "responses": {
          "200": {
            "description": "+EV opportunities.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "version",
                    "feed",
                    "generated_at",
                    "data"
                  ],
                  "properties": {
                    "version": {
                      "type": "string",
                      "const": "v1"
                    },
                    "feed": {
                      "type": "string",
                      "description": "Which feed produced this response."
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of items in `data`, on list feeds."
                    },
                    "filtered": {
                      "type": "string",
                      "description": "Set when the caller's tier trimmed the payload. The free tier returns `game-lines-only`."
                    },
                    "upstream_generated_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "model_and_market",
                        "market_only"
                      ],
                      "properties": {
                        "model_and_market": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EvOpportunity"
                          }
                        },
                        "market_only": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EvOpportunity"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's plan does not include this feed (`upgrade_required: true`, `required_tier` names the plan that does).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-minute limit exceeded (retry after the `Retry-After` header), or the monthly allowance is used up (`quota_exceeded: true`, `resets_at` is when it refills: 00:00 UTC on the 1st).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "502": {
            "description": "The upstream feed was unavailable. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/props": {
      "get": {
        "operationId": "listPlayerProps",
        "summary": "List player-prop markets for one sport (Starter plan)",
        "description": "Every player-prop market for the sport, every bookmaker, every line. Requires the Starter plan.",
        "tags": [
          "odds"
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "required": true,
            "description": "Sport key, lower case. An unsupported key returns 400 with the list of valid ones.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z_]+$"
            },
            "example": "afl"
          }
        ],
        "responses": {
          "200": {
            "description": "Player-prop markets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "version",
                    "feed",
                    "generated_at",
                    "data"
                  ],
                  "properties": {
                    "version": {
                      "type": "string",
                      "const": "v1"
                    },
                    "feed": {
                      "type": "string",
                      "description": "Which feed produced this response."
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of items in `data`, on list feeds."
                    },
                    "filtered": {
                      "type": "string",
                      "description": "Set when the caller's tier trimmed the payload. The free tier returns `game-lines-only`."
                    },
                    "data": {
                      "description": "Player-prop markets for the sport, as produced by the feed."
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "400": {
            "description": "`sport` is missing or not one of the listed values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's plan does not include this feed (`upgrade_required: true`, `required_tier` names the plan that does).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-minute limit exceeded (retry after the `Retry-After` header), or the monthly allowance is used up (`quota_exceeded: true`, `resets_at` is when it refills: 00:00 UTC on the 1st).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "502": {
            "description": "The upstream feed was unavailable. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/arbs": {
      "get": {
        "operationId": "listArbitrage",
        "summary": "List cross-bookmaker arbitrage opportunities (Pro plan)",
        "description": "Markets where backing every outcome at different bookmakers returns more than the stake, with the price and bookmaker for each leg. Requires the Pro plan.",
        "tags": [
          "odds"
        ],
        "parameters": [
          {
            "name": "min",
            "in": "query",
            "required": false,
            "description": "Minimum profit, in percent of total stake. Defaults to the service minimum.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1
          }
        ],
        "responses": {
          "200": {
            "description": "Arbitrage opportunities.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "version",
                    "feed",
                    "generated_at",
                    "data"
                  ],
                  "properties": {
                    "version": {
                      "type": "string",
                      "const": "v1"
                    },
                    "feed": {
                      "type": "string",
                      "description": "Which feed produced this response."
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of items in `data`, on list feeds."
                    },
                    "filtered": {
                      "type": "string",
                      "description": "Set when the caller's tier trimmed the payload. The free tier returns `game-lines-only`."
                    },
                    "min_profit_pct": {
                      "type": "number"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Arbitrage"
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's plan does not include this feed (`upgrade_required: true`, `required_tier` names the plan that does).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-minute limit exceeded (retry after the `Retry-After` header), or the monthly allowance is used up (`quota_exceeded: true`, `resets_at` is when it refills: 00:00 UTC on the 1st).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "502": {
            "description": "The upstream feed was unavailable. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sgm-quote": {
      "post": {
        "operationId": "quoteSameGameMulti",
        "summary": "Price a same-game multi across bookmakers (Pro plan)",
        "description": "Live cross-bookmaker price for a same-game multi of two or more legs. Requires the Pro plan. Never cached.",
        "tags": [
          "odds"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SgmQuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "version",
                    "feed",
                    "generated_at",
                    "data"
                  ],
                  "properties": {
                    "version": {
                      "type": "string",
                      "const": "v1"
                    },
                    "feed": {
                      "type": "string",
                      "description": "Which feed produced this response."
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of items in `data`, on list feeds."
                    },
                    "filtered": {
                      "type": "string",
                      "description": "Set when the caller's tier trimmed the payload. The free tier returns `game-lines-only`."
                    },
                    "data": {
                      "description": "Per-bookmaker multi prices, as produced by the pricer."
                    }
                  }
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "400": {
            "description": "Body is not JSON, or lacks sport, home, away and at least two legs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key's plan does not include this feed (`upgrade_required: true`, `required_tier` names the plan that does).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Per-minute limit exceeded (retry after the `Retry-After` header), or the monthly allowance is used up (`quota_exceeded: true`, `resets_at` is when it refills: 00:00 UTC on the 1st).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              }
            }
          },
          "502": {
            "description": "The upstream feed was unavailable. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/waitlist": {
      "post": {
        "operationId": "requestApiAccess",
        "summary": "Send a bookmaker desk-feed enquiry",
        "description": "For bookmaker trading teams asking about the desk feed (requested_tier `desk`, company required). API keys are self-serve at https://www.haterpicks.app/developer. No key needed. Limited to 5 requests a minute per IP.",
        "tags": [
          "access"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WaitlistRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WaitlistResponse"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "Body is not JSON, or the email is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "503": {
            "description": "Could not record the request. Try again shortly.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlainError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimit-Policy": {
        "description": "Per-minute limit, IETF RateLimit header fields draft: `\"minute\";q=<requests>;w=60`.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit": {
        "description": "What is left of the per-minute limit: `\"minute\";r=<remaining>;t=<seconds until a request frees up>`.",
        "schema": {
          "type": "string"
        }
      },
      "X-RateLimit-Limit": {
        "description": "Requests allowed per minute.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left this minute.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Seconds until a request frees up.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Limit": {
        "description": "Calls the plan allows this month.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Remaining": {
        "description": "Calls left this month.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Reset": {
        "description": "When the monthly allowance resets (ISO 8601, UTC).",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "securitySchemes": {
      "bearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your HaterPicks API key (starts `hp_live_`) as a Bearer token."
      },
      "headerKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your HaterPicks API key (starts `hp_live_`) in the x-api-key header."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "version",
          "feed",
          "error"
        ],
        "properties": {
          "version": {
            "type": "string",
            "const": "v1"
          },
          "feed": {
            "type": "string"
          },
          "error": {
            "type": "string",
            "description": "What went wrong, in plain English."
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "pro",
              "business"
            ]
          },
          "upgrade_required": {
            "type": "boolean"
          },
          "required_tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "pro",
              "business"
            ],
            "description": "The cheapest plan that includes the feed."
          },
          "quota_exceeded": {
            "type": "boolean",
            "description": "The monthly allowance is used up."
          },
          "resets_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the monthly allowance refills."
          }
        }
      },
      "PlainError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "RateLimited": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "description": "The Retry-After response header gives the wait in seconds."
      },
      "BookmakerPrice": {
        "type": "object",
        "description": "One bookmaker's decimal prices for a row. A bookmaker with no price is absent from `prices`.",
        "properties": {
          "over": {
            "type": [
              "number",
              "null"
            ],
            "description": "Over / home / first-named side."
          },
          "under": {
            "type": [
              "number",
              "null"
            ],
            "description": "Under / away / second-named side."
          },
          "draw": {
            "type": [
              "number",
              "null"
            ],
            "description": "Draw, on three-way markets only."
          },
          "over_link": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "under_link": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "draw_link": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "scraped_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "OddsRow": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "market",
          "player_or_team",
          "line",
          "game_start_iso",
          "game_key",
          "prices"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "sport": {
            "type": "string",
            "description": "Sport code, upper case (for example `AFL`). The `sport` filter matches it."
          },
          "market": {
            "type": "string",
            "description": "Market key, e.g. `H2H`, `TOTAL`, `LINE`, `AFL_DISPOSALS`."
          },
          "player_or_team": {
            "type": "string",
            "description": "The selection: a player on props, the home team on game lines."
          },
          "team": {
            "type": "string"
          },
          "matchup": {
            "type": "string"
          },
          "away_team": {
            "type": "string",
            "description": "Present on game lines."
          },
          "line": {
            "type": "number",
            "description": "The handicap or total. 0 on head-to-head."
          },
          "game_start_iso": {
            "type": "string",
            "format": "date-time"
          },
          "game_key": {
            "type": "string"
          },
          "prices": {
            "type": "object",
            "description": "Keyed by bookmaker id.",
            "additionalProperties": {
              "$ref": "#/components/schemas/BookmakerPrice"
            }
          }
        }
      },
      "EvOpportunity": {
        "type": "object",
        "description": "One +EV player-prop price. Fields beyond these may be added within v1.",
        "additionalProperties": true
      },
      "Arbitrage": {
        "type": "object",
        "required": [
          "sport",
          "market",
          "selection",
          "game_start_iso",
          "line",
          "profit_pct",
          "legs"
        ],
        "properties": {
          "sport": {
            "type": "string"
          },
          "market": {
            "type": "string"
          },
          "selection": {
            "type": "string"
          },
          "matchup": {
            "type": [
              "string",
              "null"
            ]
          },
          "game_start_iso": {
            "type": "string",
            "format": "date-time"
          },
          "line": {
            "type": "number"
          },
          "profit_pct": {
            "type": "number",
            "description": "Guaranteed return as a percent of total stake."
          },
          "legs": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "side",
                "bookie",
                "odds"
              ],
              "properties": {
                "side": {
                  "type": "string"
                },
                "bookie": {
                  "type": "string"
                },
                "odds": {
                  "type": "number",
                  "description": "Decimal price."
                }
              }
            }
          }
        }
      },
      "SgmQuoteRequest": {
        "type": "object",
        "required": [
          "sport",
          "home",
          "away",
          "legs"
        ],
        "properties": {
          "sport": {
            "type": "string",
            "description": "Sport key, lower case."
          },
          "home": {
            "type": "string",
            "description": "Home team name."
          },
          "away": {
            "type": "string",
            "description": "Away team name."
          },
          "legs": {
            "type": "array",
            "minItems": 2,
            "description": "The selections to combine, each a player/team market, side and line.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "books": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Limit the quote to these bookmaker ids. Omit for every bookmaker."
          }
        }
      },
      "WaitlistRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "requested_tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "pro",
              "business",
              "desk"
            ],
            "default": "desk"
          },
          "use_case": {
            "type": "string",
            "maxLength": 1000
          },
          "company": {
            "type": "string",
            "maxLength": 200
          }
        }
      },
      "WaitlistResponse": {
        "type": "object",
        "required": [
          "ok",
          "requested_tier",
          "message"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "requested_tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "pro",
              "business",
              "desk"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "version",
          "status",
          "documentation",
          "openapi",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "const": "v1"
          },
          "status": {
            "type": "string",
            "const": "ok"
          },
          "description": {
            "type": "string"
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "request_access": {
            "type": "string",
            "format": "uri"
          },
          "authentication": {
            "type": "string"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "method",
                "path",
                "operationId",
                "auth",
                "tier",
                "summary"
              ],
              "properties": {
                "method": {
                  "type": "string",
                  "enum": [
                    "GET",
                    "POST"
                  ]
                },
                "path": {
                  "type": "string"
                },
                "operationId": {
                  "type": "string"
                },
                "auth": {
                  "type": "boolean",
                  "description": "Whether the call needs an API key."
                },
                "tier": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "free",
                    "starter",
                    "pro",
                    "business",
                    null
                  ]
                },
                "summary": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}