# KickoffAPI — Kotlin SDK

One file, the JDK's own `java.net.http.HttpClient`, no Gradle or Maven dependency
to add. JDK 11+ (and Android API 34+).

```kotlin
import java.net.URI   // nothing else — no package to import

val api = KickoffApi("YOUR_API_KEY")            // free key: https://kickoffapi.com/portal/

val live = api.countries()                      // raw JSON body as a String
val epl  = api.standings(mapOf("league" to "lg_8K3mZ0pQ", "season" to 2025))
val fx   = api.fixture("fx_99Xy")

api.shutdown()                                  // releases the connection pool
```

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 base URL to the constructor 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).

## Why the response is a raw string

The JDK ships no JSON library, and depending on one would defeat the point of a
dependency-free client — so each method returns the raw body as a `String` and
you decode it with whatever your project already has:

```kotlin
// Android
val json = JSONObject(api.countries())
val data = json.getJSONArray("data")

// kotlinx.serialization
@Serializable data class Country(val name: String, val code: String, val flag: String?)
@Serializable data class Envelope<T>(val data: List<T>)
val rows = Json.decodeFromString<Envelope<Country>>(api.countries()).data
```

## 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 Kotlin
camelCase. `get()`/`post()` are there for anything without a named method yet.

```kotlin
api.countries(mapOf("limit" to 50))
api.leagues(mapOf("country" to "England"))
api.fixtures(mapOf("league" to "lg_8K3mZ0pQ", "season" to 2025, "limit" to 20))
api.fixture_events("fx_99Xy", mapOf("refresh" to 1))
api.top_scorers(mapOf("league" to "lg_8K3mZ0pQ", "season" to 2025))
api.odds(mapOf("fixture" to "fx_99Xy"))
api.xg(mapOf("x" to 110, "y" to 38, "bodyPart" to "Right Foot", "situation" to "Open Play"))
api.xg_batch(listOf(Shot(110.0, 38.0, "Right Foot", "Open Play"), Shot(118.0, 41.0)))
api.resolve(mapOf("entity" to "team", "legacyId" to 33))
api.account_status()
```

## Errors

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

```kotlin
try {
    val fx = api.fixture("fx_does_not_exist")
} catch (e: KickoffApiException) {
    println(e.status)            // 404
    println(e.message)           // fixture not found
}

try {
    api.countries(mapOf("limit" to 5000))
} catch (e: KickoffApiException) {
    if (e.status == 429 && e.retryAfterSeconds != null) {
        Thread.sleep(e.retryAfterSeconds!!.toLong() * 1000)
    }
}
```

Spec: `/openapi-v2.yaml`.
