A Developer’s Guide: Leveraging Sportmonks Football API in Kotlin
Contents

What you need

  • JDK 17 or later
  • Kotlin 1.9+ (via IntelliJ IDEA, Android Studio, or the Kotlin command-line compiler)
  • A Sportmonks API token from your MySportmonks account

The Football API covers more than 2,200 leagues and cups, and every endpoint in this guide works on the free plan.

Setting up the project

Create a Gradle project and add OkHttp for the HTTP call and kotlinx.serialization for parsing the JSON response. In build.gradle.kts:

plugins {
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.serialization") version "1.9.22"
}

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}

OkHttp handles the request and response, and kotlinx.serialization turns the JSON body into Kotlin objects without an intermediate step through a generic map.

Modelling the response

Our Football API returns a data array of fixtures, and each fixture can be enriched with related entities using include. For this guide we will request participants (the two teams) and scores (the score per period). Model just the fields you need:

import kotlinx.serialization.Serializable

@Serializable
data class FixtureResponse(val data: List<Fixture>)

@Serializable
data class Fixture(
    val id: Int,
    val name: String,
    val starting_at: String,
    val participants: List<Participant> = emptyList(),
    val scores: List<Score> = emptyList()
)

@Serializable
data class Participant(
    val id: Int,
    val name: String,
    val meta: ParticipantMeta? = null
)

@Serializable
data class ParticipantMeta(val location: String? = null)

@Serializable
data class Score(
    val description: String,
    val score: ScoreValue
)

@Serializable
data class ScoreValue(val participant: String, val goals: Int)

Keeping the data classes narrow like this means the app only breaks if a field you actually use changes, not every time the API adds something new elsewhere in the response.

Making the request

The fixtures-by-date-range endpoint takes a start and end date in the URL path, plus your token and includes as query parameters:

import okhttp3.OkHttpClient
import okhttp3.Request
import kotlinx.serialization.json.Json

fun main() {
    val token = "YOUR_API_TOKEN"
    val startDate = "2026-09-12"
    val endDate = "2026-09-14"

    val url = "https://api.sportmonks.com/v3/football/fixtures/between/$startDate/$endDate" +
        "?api_token=$token&include=participants;scores"

    val client = OkHttpClient()
    val request = Request.Builder().url(url).build()

    client.newCall(request).execute().use { response ->
        val body = response.body?.string() ?: return
        val json = Json { ignoreUnknownKeys = true }
        val fixtures = json.decodeFromString<FixtureResponse>(body).data

        fixtures.forEach { fixture ->
            val home = fixture.participants.firstOrNull { it.meta?.location == "home" }?.name ?: "Home"
            val away = fixture.participants.firstOrNull { it.meta?.location == "away" }?.name ?: "Away"
            println("${fixture.starting_at} - $home vs $away")
        }
    }
}

ignoreUnknownKeys = true matters here: the API response includes far more fields than the ones modelled above, and without that setting the deserialiser would fail on the first field it does not recognise. Semicolons separate multiple includes in the same request, so include=participants;scores fetches both in a single call rather than two round trips.

Reading the score

The scores array returns one entry per period (current, first half, second half, and so on), each tied to a participant ID rather than a team name. To print a final score, filter for the CURRENT description and match the participant ID back to the team you already resolved:

val current = fixture.scores.filter { it.description == "CURRENT" }
val homeGoals = current.firstOrNull { it.score.participant == "home" }?.score?.goals ?: 0
val awayGoals = current.firstOrNull { it.score.participant == "away" }?.score?.goals ?: 0
println("$home $homeGoals - $awayGoals $away")

This pattern, filtering an included array by a description or type field rather than assuming a fixed order, applies to most of the API’s related entities, including events, statistics and odds.

Handling errors and rate limits

A malformed token or an over-limit request returns a non-200 status with a JSON error body rather than throwing at the HTTP layer, so check response.isSuccessful before you try to decode anything:

if (!response.isSuccessful) {
    println("Request failed: ${response.code} ${response.body?.string()}")
    return
}

Your plan determines how many requests per hour you get and how many leagues you can query, and both are visible in your MySportmonks dashboard. If you are polling for live updates rather than pre-match fixtures, use the dedicated /livescores endpoints instead of repeatedly calling fixtures by date, since they are built for frequent polling and return only what changed.

Making the call non-blocking

The example above runs on the main thread, which is fine for a command-line script but not for an Android app, where a blocking network call on the main thread triggers an ANR. Wrap the same OkHttp call in a coroutine using Dispatchers.IO, and suspend rather than block while the request is in flight:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

suspend fun fetchFixtures(startDate: String, endDate: String, token: String): List<Fixture> =
    withContext(Dispatchers.IO) {
        val url = "https://api.sportmonks.com/v3/football/fixtures/between/$startDate/$endDate" +
            "?api_token=$token&include=participants;scores"

        val client = OkHttpClient()
        val request = Request.Builder().url(url).build()

        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) return@withContext emptyList()
            val body = response.body?.string() ?: return@withContext emptyList()
            Json { ignoreUnknownKeys = true }.decodeFromString<FixtureResponse>(body).data
        }
    }

Call this from a ViewModel with viewModelScope.launch { }, or from a backend service with any coroutine scope you already have, and the parsing logic from the earlier sections stays exactly the same. Only where the call happens changes.

Where to take this next

The same request and parsing pattern extends to almost anything else in the Football API: swap the includes for predictions to add win probabilities, statistics for match stats, or standings on the leagues endpoint to build a table view. Our documentation covers every endpoint and include option in detail, and the Postman collection linked from each endpoint page is a fast way to check a response shape before you write the matching data class.

If you are building for Android specifically, this same OkHttp and kotlinx.serialization setup drops into a ViewModel or repository layer without changes, since neither library depends on anything JVM-server-specific.

Faq

Are there any limitations or restrictions when using Sportmonks Football API with Postman?
Sportmonks Football API does not impose any specific limitations or restrictions on using Postman with the API. However, you should be mindful of your API usage limits based on your subscription plan. Additionally, ensure that you handle sensitive information such as API keys securely within Postman to prevent unauthorized access or misuse.
Can I keep my current API token?
You can use the same API token as you are currently using for API 2.0. You don't need to create another token for API 3.0.
Do I need an API token for the Football Widgets to work?
Maintenance is done by our developers, and the widgets will request our API every time it is needed. Therefore, no API token is required.
How can I test the Sportmonks API for FREE?
You have two options:
  1. Create an account on My Sportmonks and get immediate access to our free plan.
  2. Subscribe to one of our paid plans and receive a one-time-only 14-day free trial.
How do I build and filter API requests?
Use the base URL and include necessary data points like scores, events, and participants. Apply filters to refine the data to match specific criteria. Here’s an example of how to incorporate the filter into your request: https://api.sportmonks.com/v3/football/livescores/inplay?api_token=YOUR_TOKEN&include=events&filters=eventTypes:14,19,20,21 By utilising filtering capabilities, we can tailor the data retrieved from the API to match the specific needs and preferences of our livescore website. For further details on filtering techniques, refer to our dedicated filter tutorial for additional guidance.
As users, what if we get stuck and need help?
Our responsive customer support team is a trademark of our business. So, whether you're using our 5-star user documentation or chatting with our agents, you can get applications up and running in no time.

Written by David Jaja

David Jaja is a technical content manager at Sportmonks, where he makes complex football data easier to understand for developers and businesses. With a background in frontend development and technical writing, he helps bridge the gap between technology and sports data. Through clear, insightful content, he ensures Sportmonks' APIs are accessible and easy to use, empowering developers to build standout football applications