# KickoffAPI — C# SDK

One file, .NET's own `HttpClient`, no NuGet package to add. Usable from Unity
(drop it in `Assets/`) and from any .NET project.

```csharp
using KickoffApi;   // or just include the file — it declares no namespace

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

var live = await api.live_scores();        // KickoffResponse — Status, Body, IsSuccess
var epl  = await api.standings("lg_8K3mZ0pQ", 2025);
var fx   = await api.fixture("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 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

`KickoffResponse.Body` is the raw JSON, not a deserialised object. Unity's .NET
Standard 2.0 profile has no JSON library in the box, and depending on one would
defeat the point of a drop-in client — so the SDK hands you the bytes and lets
your project use whatever it already has:

```csharp
public class Envelope<T> { public T[] data; }

// Unity: JsonUtility
var res = await api.countries();
var rows = JsonUtility.FromJson<Envelope<Country>>(res.Body);

// .NET 6+: System.Text.Json
var doc = System.Text.Json.JsonDocument.Parse(res.Body);
var countries = doc.RootElement.GetProperty("data").Deserialize<Country[]>();
```

## 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 C#
PascalCase. `get_async()`/`post_async()` are there for anything without a named
method yet.

```csharp
await api.countries(limit: 50);
await api.leagues(country: "England");
await api.fixtures(league: "lg_8K3mZ0pQ", season: 2025, limit: 20);
await api.fixture_events("fx_99Xy", refresh: true);
await api.top_scorers("lg_8K3mZ0pQ", 2025);
await api.odds("fx_99Xy");
await api.xg(110, 38, bodyPart: "Right Foot", situation: "Open Play");
await api.xg_batch(new[] { new Shot { X = 110, Y = 38 }, new Shot { X = 118, Y = 41 } });
await api.resolve("team", 33);
await 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:

```csharp
try
{
    var fx = await api.fixture("fx_does_not_exist");
}
catch (KickoffApiException e)
{
    Console.WriteLine(e.Status);       // 404
    Console.WriteLine(e.ErrorMessage);  // fixture not found
}

try
{
    await api.countries(limit: 5000);
}
catch (KickoffApiException e) when (e.Status == 429 && e.RetryAfterSeconds.HasValue)
{
    await Task.Delay(TimeSpan.FromSeconds(e.RetryAfterSeconds.Value));
}
```

Spec: `/openapi-v2.yaml`.
