# KickoffAPI — Swift SDK

One file, Foundation's `URLSession`, no Swift Package Manager or CocoaPods
dependency to add. iOS 15+ / macOS 12+ (async/await).

```swift
let api = KickoffAPI(apiKey: "YOUR_API_KEY")   // free key: https://kickoffapi.com/portal/

let live = try await api.live_scores()        // decoded JSON as `Any`
let epl  = try await api.standings(["league": "lg_8K3mZ0pQ", "season": 2025])
let fx   = try await api.fixture(id: "fx_99Xy")
```

Talks to **v2** by default (`https://api.kickoffapi.com/api/v2`), so every
response is the unified `{ data, meta }` envelope with v2's native ids (`lg_…`,
`tm_…`, `fx_…`). Pass a different `baseURL:` to the initializer for the legacy
API. See the [v2 reference](https://docs.kickoffapi.com/api-v2-reference.html) or
the [v1 reference](https://docs.kickoffapi.com/api-v1-reference.html) (legacy,
sunsets 01 Jan 2027).

## Typed access

Each method returns Foundation's decoded JSON as `Any`. Use the `decoding:`
overload with your own `Codable` model when you want types:

```swift
struct Country: Decodable { let name: String; let code: String; let flag: String? }
struct Envelope<Value: Decodable>: Decodable { let data: [Value] }

let countries = try await api.countries(decoding: Envelope<Country>.self).data
let standings = try await api.standings(["league": "lg_8K3mZ0pQ", "season": 2025],
                                         decoding: Envelope<StandingRow>.self)
```

## Methods

One per v2 endpoint, named exactly as the Python client names it
(`live_scores()`, `head_to_head()`, `xg_batch()`, …) so the same call reads the
same in every official SDK — which is why these are snake_case rather than Swift
camelCase. `get()`/`post()` are there for anything without a named method yet.

```swift
try await api.countries(["limit": 50])
try await api.leagues(["country": "England"])
try await api.fixtures(["league": "lg_8K3mZ0pQ", "season": 2025, "limit": 20])
try await api.fixture_events(id: "fx_99Xy", ["refresh": 1])
try await api.top_scorers(["league": "lg_8K3mZ0pQ", "season": 2025])
try await api.odds(["fixture": "fx_99Xy"])
try await api.xg(["x": 110, "y": 38, "bodyPart": "Right Foot", "situation": "Open Play"])
try await api.xg_batch(shots: [["x": 110, "y": 38], ["x": 118, "y": 41]])
try await api.resolve(["entity": "team", "legacyId": 33])
try await api.account_status()
```

## Errors

Every non-2xx response throws `KickoffAPIError`, which carries the status and the
message the API sent back. A 429 also carries the `Retry-After` wait:

```swift
do {
    let fx = try await api.fixture(id: "fx_does_not_exist")
} catch let e as KickoffAPIError {
    print(e.status)      // 404
    print(e.message)     // fixture not found
}

do {
    try await api.countries(["limit": 5000])
} catch let e as KickoffAPIError where e.status == 429 && e.retryAfter != nil {
    try await Task.sleep(nanoseconds: UInt64(e.retryAfter!) * 1_000_000_000)
}
```

Spec: `/openapi-v2.yaml`.
