{
  "openapi": "3.1.0",
  "info": {
    "title": "ArbiScan Odds API",
    "version": "1.1.0",
    "description": "Server-to-server API for fresh prematch bookmaker odds. API Data Plus adds observed history, WebSocket changes and signed webhooks. Timestamps are UTC ISO-8601 and decimal odds are numbers greater than 1.0.",
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.arbiscan.pl/v1"
    }
  ],
  "x-websocket": {
    "url": "wss://api.arbiscan.pl/v1/stream",
    "plan": "API Data Plus",
    "authentication": "X-API-Key request header",
    "queryParameters": ["cursor", "sports", "bookmakers"],
    "messageTypes": ["ready", "odds.changed", "heartbeat", "error"],
    "limits": {
      "connectionsPerAccount": 1,
      "changesPerMessage": 50,
      "pollIntervalMilliseconds": 1000
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "summary": "Feed health",
        "operationId": "getHealth",
        "security": [],
        "responses": {
          "200": {
            "description": "Current feed health",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/sports": {
      "get": {
        "summary": "List sports with fresh prematch offers",
        "operationId": "listSports",
        "responses": {
          "200": {
            "description": "Canonical sport names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["sports"],
                  "properties": {
                    "sports": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/leagues": {
      "get": {
        "summary": "List leagues with fresh prematch offers",
        "operationId": "listLeagues",
        "parameters": [
          {
            "$ref": "#/components/parameters/Sport"
          }
        ],
        "responses": {
          "200": {
            "description": "League names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["leagues"],
                  "properties": {
                    "leagues": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/bookmakers": {
      "get": {
        "summary": "List bookmakers with fresh prematch offers",
        "operationId": "listBookmakers",
        "responses": {
          "200": {
            "description": "Bookmaker names",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["bookmakers"],
                  "properties": {
                    "bookmakers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/markets": {
      "get": {
        "summary": "Discover currently available market keys",
        "operationId": "listMarkets",
        "parameters": [
          {
            "$ref": "#/components/parameters/Sport"
          },
          {
            "$ref": "#/components/parameters/League"
          },
          {
            "name": "bookmaker",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 256
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2000,
              "default": 500
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque next_cursor value from the previous catalogue page.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fresh market catalogue",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["count", "markets", "has_more", "next_cursor"],
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": ["string", "null"]
                    },
                    "markets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MarketCoverage"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/odds": {
      "get": {
        "summary": "List fresh prematch events and odds",
        "operationId": "listOdds",
        "description": "Use next_cursor until has_more is false to retrieve the complete filtered feed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Sport"
          },
          {
            "$ref": "#/components/parameters/League"
          },
          {
            "name": "bookmakers",
            "in": "query",
            "description": "Comma-separated exact bookmaker names.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "description": "Comma-separated exact market keys returned by /markets.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "commence_from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "commence_to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "max_age_seconds",
            "in": "query",
            "description": "Maximum accepted quote age in seconds. Stale data cannot be requested.",
            "schema": {
              "type": "integer",
              "minimum": 60,
              "maximum": 900,
              "default": 900
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque next_cursor value from the previous page.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fresh prematch odds page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OddsPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Snapshot changed during pagination; restart from the first page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/events/{event_id}": {
      "get": {
        "summary": "Get one fresh prematch event",
        "operationId": "getEvent",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventId"
          }
        ],
        "responses": {
          "200": {
            "description": "Event with current odds",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/events/{event_id}/history": {
      "get": {
        "summary": "Get event odds history",
        "operationId": "getEventHistory",
        "description": "Available only with API Data Plus. Returns observed price changes retained for up to 48 hours; use next_cursor until has_more is false.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventId"
          },
          {
            "name": "bookmaker",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "market",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "h2h"
            }
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5000,
              "default": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque continuation cursor. Keep event, market, bookmaker, since and until filters unchanged between pages.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Observed history rows and coverage metadata",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HistoryPage"
                }
              }
            }
          },
          "403": {
            "description": "The current plan does not include odds history",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "summary": "List API Data Plus webhook destinations",
        "operationId": "listWebhooks",
        "responses": {
          "200": {
            "description": "Webhook configurations without signing secrets"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Active API Data Plus access is required"
          }
        }
      },
      "post": {
        "summary": "Create an API Data Plus webhook destination",
        "operationId": "createWebhook",
        "description": "The HMAC signing secret is returned once in the create response.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": ["url"],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "uniqueItems": true,
                    "items": {
                      "type": "string",
                      "enum": ["odds.changed", "snapshot.updated"]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created; signing_secret is shown once"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Active API Data Plus access is required"
          },
          "409": {
            "description": "Three active webhook destinations already exist"
          }
        }
      }
    },
    "/webhooks/{webhook_id}": {
      "delete": {
        "summary": "Disable a webhook destination",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^wh_[0-9a-f]{32}$"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook disabled"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Active API Data Plus access is required"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{webhook_id}/test": {
      "post": {
        "summary": "Queue a signed test delivery",
        "operationId": "testWebhook",
        "parameters": [
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^wh_[0-9a-f]{32}$"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Test event queued"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "403": {
            "description": "Active API Data Plus access is required"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "summary": "Get usage for the current key",
        "operationId": "getUsage",
        "description": "Does not consume the data quota; protected by a separate 30 requests/minute limit.",
        "responses": {
          "200": {
            "description": "Daily and lifetime quota usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "parameters": {
      "Sport": {
        "name": "sport",
        "in": "query",
        "schema": {
          "type": "string",
          "maxLength": 128
        }
      },
      "League": {
        "name": "league",
        "in": "query",
        "schema": {
          "type": "string",
          "maxLength": 256
        }
      },
      "EventId": {
        "name": "event_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "maxLength": 512
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid query parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid, expired, disabled, or suspended-account API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Fresh prematch event not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-minute, daily, or lifetime quota exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "snapshot": {
            "$ref": "#/components/schemas/Snapshot"
          },
          "quota": {
            "$ref": "#/components/schemas/Quota"
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "ok",
          "data_fresh",
          "data_status",
          "events_known",
          "fresh_events_known",
          "stale_events_known",
          "active_max_age_seconds"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data_fresh": {
            "type": "boolean",
            "description": "True only when at least one future event has both a fresh event observation and a fresh quote."
          },
          "data_status": {
            "type": "string",
            "enum": ["fresh", "degraded"]
          },
          "events_known": {
            "type": "integer",
            "minimum": 0
          },
          "fresh_events_known": {
            "type": "integer",
            "minimum": 0
          },
          "stale_events_known": {
            "type": "integer",
            "minimum": 0
          },
          "active_max_age_seconds": {
            "type": "integer",
            "maximum": 900
          },
          "last_ingest": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "last_ingest_age_seconds": {
            "type": ["integer", "null"]
          }
        }
      },
      "MarketCoverage": {
        "type": "object",
        "required": ["key", "family", "specifier", "event_count", "bookmaker_count"],
        "properties": {
          "key": {
            "type": "string"
          },
          "family": {
            "type": "string"
          },
          "specifier": {
            "type": ["string", "null"]
          },
          "event_count": {
            "type": "integer",
            "minimum": 1
          },
          "bookmaker_count": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "Outcome": {
        "type": "object",
        "required": ["name", "price", "last_update"],
        "properties": {
          "name": {
            "type": "string"
          },
          "price": {
            "type": "number",
            "exclusiveMinimum": 1
          },
          "last_update": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Market": {
        "type": "object",
        "required": ["key", "last_update", "outcomes"],
        "properties": {
          "key": {
            "type": "string"
          },
          "last_update": {
            "type": "string",
            "format": "date-time"
          },
          "outcomes": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Outcome"
            }
          }
        }
      },
      "Bookmaker": {
        "type": "object",
        "required": ["key", "last_update", "markets"],
        "properties": {
          "key": {
            "type": "string"
          },
          "last_update": {
            "type": "string",
            "format": "date-time"
          },
          "markets": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Market"
            }
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "discipline",
          "event_scope",
          "league",
          "source_leagues",
          "home_team",
          "away_team",
          "commence_time",
          "last_seen",
          "bookmakers"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "sport": {
            "type": "string"
          },
          "discipline": {
            "type": "string"
          },
          "event_scope": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "home_team": {
            "type": "string"
          },
          "away_team": {
            "type": "string"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time"
          },
          "last_seen": {
            "type": "string",
            "format": "date-time"
          },
          "source_leagues": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bookmakers": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Bookmaker"
            }
          }
        }
      },
      "Quota": {
        "type": "object",
        "properties": {
          "used": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 1
          },
          "minute_used": {
            "type": "integer",
            "minimum": 0
          },
          "minute_limit": {
            "type": "integer",
            "minimum": 1
          },
          "minute_remaining": {
            "type": "integer",
            "minimum": 0
          },
          "total_used": {
            "type": "integer",
            "minimum": 0
          },
          "total_limit": {
            "type": ["integer", "null"],
            "minimum": 1
          },
          "remaining": {
            "type": ["integer", "null"],
            "minimum": 0
          }
        }
      },
      "OddsPage": {
        "type": "object",
        "required": [
          "count",
          "events",
          "has_more",
          "next_cursor",
          "freshness",
          "snapshot",
          "quota"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": ["string", "null"]
          },
          "freshness": {
            "type": "object",
            "required": ["mode", "max_age_seconds"],
            "properties": {
              "mode": {
                "const": "fresh_prematch"
              },
              "max_age_seconds": {
                "type": "integer",
                "minimum": 60,
                "maximum": 900
              }
            }
          },
          "snapshot": {
            "$ref": "#/components/schemas/Snapshot"
          },
          "quota": {
            "$ref": "#/components/schemas/Quota"
          }
        }
      },
      "HistoryPoint": {
        "type": "object",
        "required": ["bookmaker", "outcome", "price", "timestamp"],
        "properties": {
          "bookmaker": {
            "type": "string"
          },
          "outcome": {
            "type": "string"
          },
          "price": {
            "type": "number",
            "exclusiveMinimum": 1
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HistoryPage": {
        "type": "object",
        "required": [
          "event_id",
          "market",
          "bookmaker",
          "count",
          "has_more",
          "next_cursor",
          "retention_hours",
          "observed_from",
          "observed_until",
          "history"
        ],
        "properties": {
          "event_id": {
            "type": "string"
          },
          "market": {
            "type": "string"
          },
          "bookmaker": {
            "type": ["string", "null"]
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "maximum": 5000
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": ["string", "null"]
          },
          "retention_hours": {
            "const": 48
          },
          "observed_from": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "observed_until": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HistoryPoint"
            }
          }
        }
      },
      "Snapshot": {
        "type": ["object", "null"],
        "required": ["id", "generated_at", "accepted_at", "event_count", "outcome_count", "publication_generation", "changed_at"],
        "properties": {
          "id": {
            "type": ["string", "null"]
          },
          "generated_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "accepted_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "event_count": {
            "type": "integer",
            "minimum": 0
          },
          "outcome_count": {
            "type": "integer",
            "minimum": 0
          },
          "publication_generation": {
            "type": "integer",
            "minimum": 0,
            "description": "Changes after every accepted partial or authoritative odds write."
          },
          "changed_at": {
            "type": ["string", "null"],
            "format": "date-time"
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": ["today", "total", "expires_at", "history"],
        "properties": {
          "owner": {
            "type": "string"
          },
          "today": {
            "type": "object",
            "required": ["used", "limit"],
            "properties": {
              "used": {
                "type": "integer",
                "minimum": 0
              },
              "limit": {
                "type": "integer",
                "minimum": 1
              }
            }
          },
          "total": {
            "type": "object",
            "required": ["used", "limit", "remaining"],
            "properties": {
              "used": {
                "type": "integer",
                "minimum": 0
              },
              "limit": {
                "type": ["integer", "null"]
              },
              "remaining": {
                "type": ["integer", "null"]
              }
            }
          },
          "expires_at": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "history": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["date", "requests"],
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "requests": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          }
        }
      }
    }
  }
}
