{
  "openapi": "3.1.0",
  "info": {
    "title": "MoviOdds bet365 Soccer Odds API",
    "version": "2026.10",
    "summary": "Free real-time bet365 soccer odds: every market, pre-match and in-play.",
    "description": "MoviOdds serves exclusively bet365 soccer odds: every market bet365 prices for a match, pre-match and in-play. Load the board with GET /odds, then keep it current over the WebSocket at wss://moviodds.com/ws (send {\"op\":\"subscribe\"}, optionally with \"fixtures\" and \"groups\" lists; you receive a snapshot per match, then delta messages as prices change). WebSockets are not describable in OpenAPI, so the stream is documented at https://moviodds.com/doc.\n\nThe odds feed needs no key. Only POST /betbuilder/price needs an API key (X-API-Key).\n\nMoviOdds is an independent project and is not affiliated with or endorsed by bet365.",
    "contact": {
      "name": "MoviOdds on Discord",
      "url": "https://discord.gg/aXNfRCjtNq"
    }
  },
  "externalDocs": {
    "description": "Guide: load the board, stream changes, every market",
    "url": "https://moviodds.com/doc"
  },
  "servers": [
    {
      "url": "https://moviodds.com"
    }
  ],
  "tags": [
    {
      "name": "Odds",
      "description": "Matches and their current bet365 prices"
    },
    {
      "name": "Bet Builder",
      "description": "bet365's real combined price for a set of legs",
      "externalDocs": {
        "url": "https://moviodds.com/docs/betbuilder/"
      }
    },
    {
      "name": "Status",
      "description": "Service health"
    }
  ],
  "paths": {
    "/odds": {
      "get": {
        "tags": [
          "Odds"
        ],
        "operationId": "getOdds",
        "summary": "Every match and its current odds",
        "description": "Every catalogued match with its current quotes. Matches the scraper has not priced yet carry an empty quotes array. Use the filters to fetch a slice instead of the whole board. Rate limit: 30 requests per minute per IP; for live updates use the WebSocket instead of polling.",
        "parameters": [
          {
            "name": "in_play",
            "in": "query",
            "description": "1 = only matches currently in play.",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          },
          {
            "name": "groups",
            "in": "query",
            "description": "Comma-separated market group ids; only quotes in these groups are returned. Full Time Result is 40 before kickoff and 1777 in play. See https://moviodds.com/docs/markets/.",
            "schema": {
              "type": "string",
              "examples": [
                "40,1777"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The board (or the filtered slice).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OddsResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "security": []
      }
    },
    "/catalog": {
      "get": {
        "tags": [
          "Odds"
        ],
        "operationId": "getCatalog",
        "summary": "Every match, without odds",
        "description": "All catalogued matches (pre-match and in-play) with names, league and kickoff. Use it to find a match id.",
        "responses": {
          "200": {
            "description": "The match list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    },
                    "sweeps": {
                      "type": "integer",
                      "description": "How many catalog refreshes have run since start."
                    },
                    "fixtures": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Fixture"
                      }
                    }
                  },
                  "required": [
                    "count",
                    "fixtures"
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": []
      }
    },
    "/snapshot": {
      "get": {
        "tags": [
          "Odds"
        ],
        "operationId": "getSnapshot",
        "summary": "One match's current odds",
        "parameters": [
          {
            "name": "fi",
            "in": "query",
            "required": true,
            "description": "Match id from /catalog or /odds.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every current quote for the match.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "fixture": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of quotes."
                    },
                    "groups": {
                      "type": "integer",
                      "description": "Number of markets."
                    },
                    "server_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "polled_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When this match was last refreshed. Absent if never seen."
                    },
                    "quotes": {
                      "type": [
                        "array",
                        "null"
                      ],
                      "items": {
                        "$ref": "#/components/schemas/Quote"
                      }
                    }
                  },
                  "required": [
                    "fixture",
                    "count",
                    "groups",
                    "server_time"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": []
      }
    },
    "/betbuilder/price": {
      "post": {
        "tags": [
          "Bet Builder"
        ],
        "operationId": "priceBetBuilder",
        "summary": "bet365's Bet Builder price for one or more slips",
        "description": "Returns bet365's real combined price for each slip (2-12 legs from one pre-match match). This is not the single odds multiplied together: bet365 adjusts for linked legs. Leg ids are sel_id values from /snapshot. Needs an API key; ask on Discord.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slips": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50,
                    "items": {
                      "type": "object",
                      "properties": {
                        "fixture": {
                          "type": "string",
                          "pattern": "^[0-9]+$"
                        },
                        "legs": {
                          "type": "array",
                          "minItems": 2,
                          "maxItems": 12,
                          "items": {
                            "type": "string"
                          },
                          "description": "sel_id values."
                        }
                      },
                      "required": [
                        "fixture",
                        "legs"
                      ]
                    }
                  }
                },
                "required": [
                  "slips"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per slip, in request order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "server_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SlipResult"
                      }
                    }
                  },
                  "required": [
                    "server_time",
                    "results"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Status"
        ],
        "operationId": "getHealth",
        "summary": "Service status",
        "responses": {
          "200": {
            "description": "Status and cache size.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "examples": [
                        "ok"
                      ]
                    },
                    "warming": {
                      "type": "boolean",
                      "description": "True right after a restart while the board loads."
                    },
                    "catalog_fixtures": {
                      "type": "integer"
                    },
                    "cache_fixtures": {
                      "type": "integer"
                    },
                    "cache_quotes": {
                      "type": "integer"
                    },
                    "last_change_unix": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Only for /betbuilder/price. Ask @Movisu on Discord."
      }
    },
    "responses": {
      "Error": {
        "description": "Error.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests from this IP. Slow down, or use the WebSocket for live updates."
      }
    },
    "schemas": {
      "Fixture": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "bet365 match id."
          },
          "home": {
            "type": "string"
          },
          "away": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "kickoff_utc": {
            "type": "string",
            "format": "date-time"
          },
          "in_play": {
            "type": "boolean"
          },
          "last_seen": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "home",
          "away",
          "kickoff_utc",
          "in_play"
        ]
      },
      "Quote": {
        "type": "object",
        "description": "One price. Key a quote by (fixture_id, group_id, market_id, sel_id).",
        "properties": {
          "fixture_id": {
            "type": "string"
          },
          "group_id": {
            "type": "string",
            "description": "Market id, stable across matches and languages (e.g. 40 = Full Time Result pre-match, 1777 in play)."
          },
          "group_name": {
            "type": "string",
            "description": "Market name as bet365 shows it."
          },
          "market_id": {
            "type": "string"
          },
          "sel_id": {
            "type": "string",
            "description": "Selection id; also the leg id for Bet Builder."
          },
          "label": {
            "type": "string"
          },
          "n2": {
            "type": "string"
          },
          "handicap": {
            "type": "string"
          },
          "side": {
            "type": "string",
            "description": "Column, e.g. Over/Under, Yes/No."
          },
          "line": {
            "type": "string",
            "description": "Line, handicap or row label."
          },
          "odds_dec": {
            "type": "number"
          },
          "odds_frac": {
            "type": "string"
          },
          "suspended": {
            "type": "boolean"
          },
          "ts": {
            "type": "string",
            "format": "date-time",
            "description": "When this price last changed."
          }
        },
        "required": [
          "fixture_id",
          "group_id",
          "sel_id",
          "odds_dec",
          "ts"
        ]
      },
      "Match": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Fixture"
          },
          {
            "type": "object",
            "properties": {
              "quotes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Quote"
                }
              },
              "market_count": {
                "type": "integer",
                "description": "Markets priced for this match (unaffected by the groups filter)."
              },
              "polled_at": {
                "type": "string",
                "format": "date-time",
                "description": "When this match was last refreshed."
              }
            },
            "required": [
              "quotes",
              "market_count"
            ]
          }
        ]
      },
      "OddsResponse": {
        "type": "object",
        "properties": {
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "server_time": {
            "type": "string",
            "format": "date-time",
            "description": "Use server_time minus a quote's ts for its age."
          },
          "match_count": {
            "type": "integer",
            "description": "Matches in this response."
          },
          "catalog_matches": {
            "type": "integer",
            "description": "All catalogued matches, regardless of filters."
          },
          "cached_matches": {
            "type": "integer"
          },
          "quote_count": {
            "type": "integer"
          },
          "warming": {
            "type": "boolean",
            "description": "Present and true right after a restart; retry in about 30 seconds."
          },
          "note": {
            "type": "string"
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Match"
            }
          }
        },
        "required": [
          "generated_at",
          "server_time",
          "match_count",
          "quote_count",
          "matches"
        ]
      },
      "SlipResult": {
        "type": "object",
        "properties": {
          "fixture": {
            "type": "string"
          },
          "home": {
            "type": "string"
          },
          "away": {
            "type": "string"
          },
          "legs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "sel_id": {
                  "type": "string"
                },
                "market": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "odds_dec": {
                  "type": "number"
                }
              },
              "required": [
                "sel_id"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "priced",
              "unavailable",
              "error"
            ],
            "description": "priced: odds set. unavailable: bet365 declined the combination. error: see error and retryable."
          },
          "odds_dec": {
            "type": "number"
          },
          "odds_frac": {
            "type": "string"
          },
          "naive_odds_dec": {
            "type": "number",
            "description": "The legs' single odds multiplied, for comparison."
          },
          "priced_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string"
          },
          "retryable": {
            "type": "boolean"
          }
        },
        "required": [
          "fixture",
          "legs",
          "status"
        ]
      }
    }
  }
}
