AirplayChart Your Radio, as a Playlist

AirplayChart API

What radio stations play, as JSON. Free, with a key, and a link back to the source.

Free for any use10,000 requests a dayJSON over HTTPSRead only
On this page

Quickstart

AirplayChart listens to radio stations around the world and records what they play. This API gives you that data as JSON: stations, plays, live and weekly charts, artists, songs, playlists, genres and countries. It is free, for personal and commercial use, under the terms of use.

Base URL https://airplaychart.com/v1

  1. Create a free key

    Sign in, name a key and accept the terms on your account page. The key is shown once, so copy it then.

    Create a key

  2. Send it in a header

    Every request carries the key in the Authorization header. Keep it in an environment variable such as AIRPLAYCHART_KEY, not in your code.

    Authorization: Bearer ac_your_key
  3. Make your first call

    Ask for one station in Brazil.

    curl
    curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
      "https://airplaychart.com/v1/stations?country=BR&limit=1"
    JavaScript
    const res = await fetch("https://airplaychart.com/v1/stations?country=BR&limit=1", {
      headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
    });
    if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
    const { data, meta } = await res.json();
    Python
    import os
    import requests
    
    res = requests.get(
        "https://airplaychart.com/v1/stations",
        params={"country": "BR", "limit": 1},
        headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
        timeout=10,
    )
    res.raise_for_status()
    body = res.json()

    The answer:

    {
      "data": [
        {
          "id": 1042,
          "name": "Radio Cidade",
          "country": "BR",
          "city": "Rio de Janeiro",
          "genres": [
            "Pop"
          ],
          "language": "pt",
          "logoUrl": "https://airplaychart.com/logo/1042.webp?v=3",
          "status": "active",
          "talk": false,
          "url": "https://airplaychart.com/station/radio-cidade-1042"
        }
      ],
      "meta": {
        "nextCursor": "eyJvIjoxfQ"
      },
      "attribution": {
        "text": "Data from AirplayChart",
        "url": "https://airplaychart.com/",
        "html": "<a href=\"https://airplaychart.com/\">Data from AirplayChart</a>"
      }
    }

Authentication

Send the key in an Authorization: Bearer header or in X-API-Key. Either works:

Authorization: Bearer ac_your_key
X-API-Key: ac_your_key

Never put the key in the URL. A request with a key in the query string is refused with 400 key_in_url, because URLs end up in logs.

A key used in browser code is visible to anyone who opens the developer tools, and they can spend your limit. Call the API from your server. If you only want to show a station on a web page, use the station widget instead, which needs no key.

CORS is open (Access-Control-Allow-Origin: *), so a browser can read the answers, but that does not make a key in a page safe.

Limits

Requests per day10,000 per key, counted per UTC day
Requests per minute60 requests per key
Play history7 days back
KeysUp to 3 active keys per account
PriceFree, for any use

Every answer to a request with a valid key carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. They count the daily quota: the limit per UTC day, what is left of it, and when it resets (the next 00:00 UTC, as an ISO 8601 time). Errors given before the key is checked, such as 401 or 400 key_in_url, have none.

The per-minute limit counts requests in each clock minute (UTC) and starts again at the top of the next minute; it is not a rolling window. Refused requests count toward neither limit.

Over a limit you get 429 with a Retry-After header. Plays older than 7 days are not available on this plan. Nothing is silently invented: meta.windowStart tells you when an answer was cut.

Responses and paging

Every successful answer is a JSON object with three keys: data (one object, or a list), meta (about the answer, such as the next page) and attribution (the credit line to show). This is the whole envelope of a one-row list:

{
  "data": [
    {
      "code": "BR",
      "stations": 1210,
      "lastPlayAt": "2026-10-27T14:03:22.000Z",
      "url": "https://airplaychart.com/country/br"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  },
  "attribution": {
    "text": "Data from AirplayChart",
    "url": "https://airplaychart.com/",
    "html": "<a href=\"https://airplaychart.com/\">Data from AirplayChart</a>"
  }
}

The endpoint examples further down show only data and meta, since attribution has the same three keys on every answer. In data, a value we do not know is null, never left out. meta keys can be absent: meta.windowStart only appears when an answer was cut.

Formats

Times in answers are ISO 8601 in UTC, to the millisecond, such as 2026-10-27T14:03:22.000Z.

A time you send (since and until) can take any of three forms. A time of day needs a zone; without one the API answers 400 bad_time rather than guess your time zone. Plays never reach back more than 7 days, whatever you ask for.

FormExampleMeans
ISO 8601 with a zone2026-10-27T12:00:00Z or 2026-10-27T09:00-03:00That instant
A date2026-10-27Midnight UTC at the start of that day
Epoch milliseconds1793102400000That instant (here 12:00 UTC on 27 October 2026)

Country codes are two letters, ISO 3166, such as BR.

Paging

Lists take limit (default 50, at most 100) and an opaque cursor. Pass the meta.nextCursor of one page as the cursor of the next, until it is null. Do not build or change a cursor yourself.

GET /v1/countries?limit=2
  "meta": { "nextCursor": "eyJvIjoyfQ" }

GET /v1/countries?limit=2&cursor=eyJvIjoyfQ
  "meta": { "nextCursor": "eyJvIjo0fQ" }

and so on, until "nextCursor" is null.

The same in JavaScript, reading every page:

let cursor = null;
do {
  const url = new URL("https://airplaychart.com/v1/countries");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
  });
  if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
  const { data, meta } = await res.json();
  // use data here
  cursor = meta.nextCursor;
} while (cursor);

A few lists stop after their first 500 rows; their parameter tables say so. A list with no cursor parameter answers in one page.

Station shapes, retired stations and merged artists

Station lists (/v1/stations and the genre lists) return a summary shape: id, name, country, city, genres, language, logoUrl, status, talk and url. The detail route /v1/stations/{id} returns the full shape, which adds firstPlayAt and variety.

A retired station is not hidden or redirected. It answers with "status": "retired" and keeps its id, because the data has no pointer to a twin.

{
  "data": {
    "id": 77,
    "name": "Old FM",
    "status": "retired"
  }
}

A merged artist answers 301 with a Location header and a body that carries the new id, so follow it. A removed artist answers 410.

{
  "error": "moved",
  "message": "This resource has a new id; follow Location.",
  "id": 412
}

Attribution

Wherever you show this data, show a visible link that reads Data from AirplayChart and points to airplaychart.com. Every answer carries it as attribution, with the link text, the address and the link ready to paste as html. The terms have the details.

When an answer is about one station, song, artist or playlist, attribution.url is that thing's page on AirplayChart, so your readers land on the page your data came from. Lists point to the home page. For /v1/stations/123/plays:

{
  "attribution": {
    "text": "Data from AirplayChart",
    "url": "https://airplaychart.com/station/radio-example-123",
    "html": "<a href=\"https://airplaychart.com/station/radio-example-123\">Data from AirplayChart</a>"
  }
}

Build the link from it, so it always points to the right page:

const a = document.createElement('a');
a.href = body.attribution.url;
a.textContent = body.attribution.text;
credit.append(a);

Or paste this where the data appears:

<a href="https://airplaychart.com/">Data from AirplayChart</a>

A plain link is what we ask for, but you may add rel="nofollow" to it if your site does that for outside links.

Errors

An error answers with an HTTP status and a body of error (a stable code to test against) and message (for people, and it may change):

{
  "error": "not_found",
  "message": "No station with that id."
}

Some errors add fields of their own. A 429 says which limit was hit and when to come back, and the Retry-After header gives the same in seconds:

{
  "error": "rate_limited",
  "message": "This key is over its limit of 60 requests a minute. Try again at retryAt.",
  "limit": "minute",
  "retryAt": "2026-10-27T14:04:00.000Z"
}
StatusCodeMeaningWhat to do
301movedThe artist was merged into another. The body carries the new id; Location carries the new URL.Follow Location and store the new id.
400key_in_urlThe key was in the query string. The request is not counted.Send the key in a header, and make a new key if the URL was logged anywhere.
400bad_limitlimit is not a whole number of 1 or more.Send 1 to 100, or leave it out for 50.
400bad_cursorcursor is not one this API issued.Pass back a meta.nextCursor unchanged.
400bad_timesince or until is not an ISO 8601 time with a zone, a date, or epoch milliseconds.Add a zone, such as Z, to a time of day.
400bad_rangesince is after until.Swap them.
400bad_countrycountry is not a two-letter ISO 3166 code.Send two letters, like BR.
400bad_filterMore than one of country, genre and q on /v1/stations.Keep one of them.
400bad_queryq is missing, too short or too long.Check the limits of q on that endpoint.
400bad_typetype on /v1/search is not station, artist or song.Send one of the three.
401invalid_keyNo key, or a key that is unknown or revoked.Check the header, or make a new key on your account page.
404not_foundNo such id, week or path.Check the id and the path.
405method_not_allowedThe API only answers GET (and OPTIONS for CORS).Use GET.
410goneThe artist was removed.Drop the id from your data.
429rate_limitedOver the daily or per-minute limit. The body says which limit and carries retryAt.Wait until retryAt (or Retry-After seconds) before the next call.
500internal_errorSomething broke on our side.Try again shortly.
503api_pausedThe API is switched off for now.Try again later. Your keys are kept.

Endpoints

Every endpoint is GET and lives under the base URL above. Each one below lists its parameters, a request in curl, JavaScript and Python, and an example answer shortened to one row.

Stations

GET/v1/stations

Stations on the air, in the summary shape.

Narrow the list with one of country, genre or q. Sending more than one answers 400 bad_filter.

The order depends on the filter: with none, A to Z by name; with country or genre, the station heard most recently first; with q, stations with the biggest playlist first, then A to Z.

The summary shape has no firstPlayAt or variety; ask for one station to get those.

Parameters
NameInTypeRequiredDescriptionDefault and limits
countryquerystringoptionalOnly stations in this country.Two letters, ISO 3166, like BR.
genrequerystringoptionalOnly stations in this genre: an id from /v1/genres, main or sub. An unknown id gives an empty list.None
qquerystringoptionalOnly stations whose name matches this text.At most 100 characters.
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/stations?country=BR&limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/stations?country=BR&limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/stations",
    params={"country": "BR", "limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 1042,
      "name": "Radio Cidade",
      "country": "BR",
      "city": "Rio de Janeiro",
      "genres": [
        "Pop"
      ],
      "language": "pt",
      "logoUrl": "https://airplaychart.com/logo/1042.webp?v=3",
      "status": "active",
      "talk": false,
      "url": "https://airplaychart.com/station/radio-cidade-1042"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/stations/{id}

One station in the full shape: the list fields plus firstPlayAt and variety.

A retired station still answers, with status "retired".

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe station id.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/stations/1042"
JavaScript
const res = await fetch("https://airplaychart.com/v1/stations/1042", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/stations/1042",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": {
    "id": 1042,
    "name": "Radio Cidade",
    "country": "BR",
    "city": "Rio de Janeiro",
    "genres": [
      "Pop"
    ],
    "language": "pt",
    "logoUrl": "https://airplaychart.com/logo/1042.webp?v=3",
    "status": "active",
    "talk": false,
    "url": "https://airplaychart.com/station/radio-cidade-1042",
    "firstPlayAt": "2025-03-02T10:11:00.000Z",
    "variety": {
      "plays": 5400,
      "songs": 1320
    }
  },
  "meta": {}
}

GET/v1/stations/{id}/now

The station's newest play, if it falls inside your plan's history window.

data is null when there is none; meta.lastPlayAt still says when the station last played anything.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe station id.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/stations/1042/now"
JavaScript
const res = await fetch("https://airplaychart.com/v1/stations/1042/now", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/stations/1042/now",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": {
    "at": "2026-10-27T14:03:22.000Z",
    "artist": "Anitta",
    "title": "Envolver",
    "songId": 8841,
    "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
    "isrcs": [
      "BRUM72200123"
    ]
  },
  "meta": {
    "lastPlayAt": "2026-10-27T14:03:22.000Z"
  }
}

GET/v1/stations/{id}/plays

The station's plays, newest first.

Reads never reach past your plan's history. When a request would, meta.windowStart says where the answer was cut.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe station id.None
sincequerytimeoptionalOnly plays at or after this time.See Formats. Never reaches back more than 7 days.
untilquerytimeoptionalOnly plays at or before this time.See Formats. Never reaches back more than 7 days.
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/stations/1042/plays?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/stations/1042/plays?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/stations/1042/plays",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "at": "2026-10-27T14:03:22.000Z",
      "artist": "Anitta",
      "title": "Envolver",
      "songId": 8841,
      "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
      "isrcs": [
        "BRUM72200123"
      ]
    }
  ],
  "meta": {
    "nextCursor": "1793109802000-98211"
  }
}

GET/v1/stations/{id}/top

The station's most played tracks, most plays first.

Counts are all-time, over every play we logged for the station; the plan's history window does not apply here.

One page only: there is no cursor.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe station id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/stations/1042/top?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/stations/1042/top?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/stations/1042/top",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "artist": "Anitta",
      "title": "Envolver",
      "songId": 8841,
      "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
      "plays": 96,
      "isrcs": [
        "BRUM72200123"
      ]
    }
  ],
  "meta": {}
}

Charts

GET/v1/charts/live

The 24-hour chart across all stations, the same one the home page shows.

songId, artistId and url are null where the site has no page for that song or artist.

Parameters

No parameters.

Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/charts/live"
JavaScript
const res = await fetch("https://airplaychart.com/v1/charts/live", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/charts/live",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "rank": 1,
      "songId": 8841,
      "title": "Envolver",
      "artist": "Anitta",
      "artistId": 377,
      "stations": 212,
      "plays": 1960,
      "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
      "url": "https://airplaychart.com/song/envolver-anitta-8841"
    }
  ],
  "meta": {}
}

GET/v1/charts/weekly

The weeks that have a weekly chart, newest first.

Parameters
NameInTypeRequiredDescriptionDefault and limits
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/charts/weekly?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/charts/weekly?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/charts/weekly",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "week": "2026-10-19",
      "stations": 5870,
      "topTitle": "Envolver",
      "topArtist": "Anitta",
      "url": "https://airplaychart.com/charts/weekly/2026-10-19"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/charts/weekly/{weekStart}

One weekly chart, with each row's previous rank, peak and weeks on chart, plus the debuts.

Parameters
NameInTypeRequiredDescriptionDefault and limits
weekStartpathstringrequiredThe Monday the week starts on, in UTC.YYYY-MM-DD, a week listed by /v1/charts/weekly.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/charts/weekly/2026-10-19"
JavaScript
const res = await fetch("https://airplaychart.com/v1/charts/weekly/2026-10-19", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/charts/weekly/2026-10-19",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": {
    "week": "2026-10-19",
    "stationsHeard": 5870,
    "url": "https://airplaychart.com/charts/weekly/2026-10-19",
    "chart": [
      {
        "rank": 1,
        "songId": 8841,
        "title": "Envolver",
        "artist": "Anitta",
        "artistId": 377,
        "stations": 212,
        "plays": 1960,
        "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
        "url": "https://airplaychart.com/song/envolver-anitta-8841",
        "previousRank": 2,
        "peak": 1,
        "weeksOn": 4
      }
    ],
    "debuts": [
      {
        "rank": 17,
        "songId": 9120,
        "title": "Nova",
        "artist": "Lia",
        "artistId": 377,
        "stations": 212,
        "plays": 1960,
        "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
        "url": null
      }
    ]
  },
  "meta": {}
}

GET/v1/charts/adds

Songs that stations started playing recently, with the stations that added them.

A country with no list answers 404.

Parameters
NameInTypeRequiredDescriptionDefault and limits
countryquerystringoptionalOne country's list. Without it you get the worldwide list.Two letters, ISO 3166, like BR.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/charts/adds?country=BR"
JavaScript
const res = await fetch("https://airplaychart.com/v1/charts/adds?country=BR", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/charts/adds",
    params={"country": "BR"},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "rank": 1,
      "songId": 8841,
      "title": "Envolver",
      "artist": "Anitta",
      "artistId": 377,
      "stations": 212,
      "plays": 1960,
      "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
      "url": "https://airplaychart.com/song/envolver-anitta-8841",
      "added": 14,
      "firstAddedAt": "2026-10-25T08:00:00.000Z",
      "addedBy": [
        {
          "id": 1042,
          "name": "Radio Cidade",
          "url": "https://airplaychart.com/station/radio-cidade-1042"
        }
      ]
    }
  ],
  "meta": {
    "country": "BR"
  }
}

Search and on-air

GET/v1/on-air

What stations are playing right now: one row per station heard in the last 30 minutes.

One page only: there is no cursor.

Parameters
NameInTypeRequiredDescriptionDefault and limits
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/on-air?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/on-air?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/on-air",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 1042,
      "name": "Radio Cidade",
      "country": "BR",
      "url": "https://airplaychart.com/station/radio-cidade-1042",
      "artist": "Anitta",
      "title": "Envolver",
      "at": "2026-10-27T14:03:22.000Z"
    }
  ],
  "meta": {
    "windowMinutes": 30
  }
}

Search stations, artists or songs by name, one kind per call.

Each type has its own row shape; the examples below show all three.

Parameters
NameInTypeRequiredDescriptionDefault and limits
qquerystringrequiredThe text to search for.At least 2 characters; longer than 80 is cut to 80.
typequeryenumrequiredWhat to search: station, artist or song.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.With type=artist or type=song, this list stops after its first 500 rows.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/search?q=anitta&type=artist&limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/search?q=anitta&type=artist&limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/search",
    params={"q": "anitta", "type": "artist", "limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response, type=artist
{
  "data": [
    {
      "id": 377,
      "name": "Anitta",
      "plays": 18200,
      "stations": 1403,
      "url": "https://airplaychart.com/artist/anitta-377"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}
Example response, type=station
{
  "data": [
    {
      "id": 1042,
      "name": "Radio Cidade",
      "country": "BR",
      "url": "https://airplaychart.com/station/radio-cidade-1042"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}
Example response, type=song
{
  "data": [
    {
      "id": 8841,
      "title": "Envolver",
      "artist": "Anitta",
      "plays": 1960,
      "stations": 212,
      "url": "https://airplaychart.com/song/envolver-anitta-8841"
    }
  ],
  "meta": {
    "nextCursor": null
  }
}

Artists

GET/v1/artists/{id}

One artist with its totals.

A merged artist answers 301 with the new id and a Location header; a removed one answers 410. The same goes for the three routes below.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe artist id.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/artists/377"
JavaScript
const res = await fetch("https://airplaychart.com/v1/artists/377", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/artists/377",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": {
    "id": 377,
    "name": "Anitta",
    "plays": 18200,
    "stations": 1403,
    "countries": 61,
    "songs": 88,
    "lastHeardAt": "2026-10-27T14:03:22.000Z",
    "url": "https://airplaychart.com/artist/anitta-377"
  },
  "meta": {}
}

GET/v1/artists/{id}/songs

The artist's songs, most played first.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe artist id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.This list stops after its first 500 rows.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/artists/377/songs?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/artists/377/songs?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/artists/377/songs",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 8841,
      "title": "Envolver",
      "artist": "Anitta",
      "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
      "plays": 1960,
      "isrcs": [
        "BRUM72200123"
      ],
      "url": "https://airplaychart.com/song/envolver-anitta-8841"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/artists/{id}/stations

The stations that play the artist, most plays first.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe artist id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.This list stops after its first 500 rows.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/artists/377/stations?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/artists/377/stations?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/artists/377/stations",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 1042,
      "name": "Radio Cidade",
      "country": "BR",
      "plays": 96,
      "url": "https://airplaychart.com/station/radio-cidade-1042"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/artists/{id}/countries

The countries the artist is played in.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe artist id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/artists/377/countries?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/artists/377/countries?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/artists/377/countries",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "code": "BR",
      "stations": 1210,
      "plays": 5120,
      "url": "https://airplaychart.com/country/br"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

Songs

GET/v1/songs/{id}

One song with its totals and ISRC codes.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe song id.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/songs/8841"
JavaScript
const res = await fetch("https://airplaychart.com/v1/songs/8841", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/songs/8841",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": {
    "id": 8841,
    "title": "Envolver",
    "artist": "Anitta",
    "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
    "plays": 1960,
    "stations": 212,
    "countries": 14,
    "firstHeardAt": "2026-09-02T09:00:00.000Z",
    "lastHeardAt": "2026-10-27T14:03:22.000Z",
    "isrcs": [
      "BRUM72200123"
    ],
    "url": "https://airplaychart.com/song/envolver-anitta-8841"
  },
  "meta": {}
}

GET/v1/songs/{id}/stations

The stations that play the song, most plays first.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe song id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.This list stops after its first 500 rows.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/songs/8841/stations?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/songs/8841/stations?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/songs/8841/stations",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 1042,
      "name": "Radio Cidade",
      "country": "BR",
      "plays": 96,
      "lastHeardAt": "2026-10-27T14:03:22.000Z",
      "url": "https://airplaychart.com/station/radio-cidade-1042"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/songs/{id}/countries

The countries the song is played in.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe song id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/songs/8841/countries?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/songs/8841/countries?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/songs/8841/countries",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "code": "BR",
      "stations": 1210,
      "plays": 5120,
      "url": "https://airplaychart.com/country/br"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

Playlists

GET/v1/playlists

Published Spotify playlists, one per station.

Parameters
NameInTypeRequiredDescriptionDefault and limits
countryquerystringoptionalOnly playlists of stations in this country.Two letters, ISO 3166, like BR.
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/playlists?country=BR&limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/playlists?country=BR&limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/playlists",
    params={"country": "BR", "limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 12,
      "name": "Radio Cidade Top 30",
      "country": "BR",
      "coverUrl": "https://airplaychart.com/cover/12.jpg",
      "spotifyUrl": "https://open.spotify.com/playlist/37i9dQZF1DX0XUsuxWHRQd",
      "followers": 1840,
      "trackCount": 30,
      "url": "https://airplaychart.com/playlist/radio-cidade-top-30-12"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/playlists/{id}

One playlist.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe playlist id.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/playlists/12"
JavaScript
const res = await fetch("https://airplaychart.com/v1/playlists/12", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/playlists/12",
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": {
    "id": 12,
    "name": "Radio Cidade Top 30",
    "country": "BR",
    "coverUrl": "https://airplaychart.com/cover/12.jpg",
    "spotifyUrl": "https://open.spotify.com/playlist/37i9dQZF1DX0XUsuxWHRQd",
    "followers": 1840,
    "trackCount": 30,
    "url": "https://airplaychart.com/playlist/radio-cidade-top-30-12"
  },
  "meta": {}
}

GET/v1/playlists/{id}/tracks

The playlist's tracks, in playlist order.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathintegerrequiredThe playlist id.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/playlists/12/tracks?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/playlists/12/tracks?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/playlists/12/tracks",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "position": 1,
      "artist": "Anitta",
      "title": "Envolver",
      "spotifyTrackId": "0a1b2c3d4e5f6g7h8i9j0k",
      "songId": 8841,
      "stations": 212,
      "countries": 14,
      "plays": 1960,
      "isrcs": [
        "BRUM72200123"
      ]
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

Genres and countries

GET/v1/genres

The genres that have stations: main genres, each followed by its sub-genres.

Use an id from here in /v1/stations?genre= or /v1/genres/{id}/stations.

Parameters
NameInTypeRequiredDescriptionDefault and limits
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/genres?limit=2"
JavaScript
const res = await fetch("https://airplaychart.com/v1/genres?limit=2", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/genres",
    params={"limit": 2},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": "pop",
      "name": "Pop",
      "level": "main",
      "parent": null,
      "url": "https://airplaychart.com/genre/pop",
      "stations": 3120
    },
    {
      "id": "dance-pop",
      "name": "Dance-Pop",
      "level": "sub",
      "parent": "pop",
      "url": "https://airplaychart.com/genre/pop/dance-pop",
      "stations": 410
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoyfQ"
  }
}

GET/v1/genres/{id}/stations

The stations in a genre, in the summary shape.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathstringrequiredA genre id from /v1/genres, main or sub, like pop or dance-pop.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/genres/pop/stations?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/genres/pop/stations?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/genres/pop/stations",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 1042,
      "name": "Radio Cidade",
      "country": "BR",
      "city": "Rio de Janeiro",
      "genres": [
        "Pop"
      ],
      "language": "pt",
      "logoUrl": "https://airplaychart.com/logo/1042.webp?v=3",
      "status": "active",
      "talk": false,
      "url": "https://airplaychart.com/station/radio-cidade-1042"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/genres/{id}/songs

The most played songs in a genre.

Parameters
NameInTypeRequiredDescriptionDefault and limits
idpathstringrequiredA genre id from /v1/genres, main or sub, like pop or dance-pop.None
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.This list stops after its first 500 rows.
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/genres/pop/songs?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/genres/pop/songs?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/genres/pop/songs",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "id": 8841,
      "title": "Envolver",
      "artist": "Anitta",
      "stations": 212,
      "plays": 1960,
      "url": "https://airplaychart.com/song/envolver-anitta-8841"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}

GET/v1/countries

The countries with at least one station on the air.

Parameters
NameInTypeRequiredDescriptionDefault and limits
limitqueryintegeroptionalHow many rows to return.Default 50. A whole number from 1; above 100 is cut to 100.
cursorquerycursoroptionalThe meta.nextCursor of the previous page, passed back unchanged.None
Example request
curl
curl -H "Authorization: Bearer $AIRPLAYCHART_KEY" \
  "https://airplaychart.com/v1/countries?limit=1"
JavaScript
const res = await fetch("https://airplaychart.com/v1/countries?limit=1", {
  headers: { Authorization: `Bearer ${process.env.AIRPLAYCHART_KEY}` },
});
if (!res.ok) throw new Error(`AirplayChart API: HTTP ${res.status}`);
const { data, meta } = await res.json();
Python
import os
import requests

res = requests.get(
    "https://airplaychart.com/v1/countries",
    params={"limit": 1},
    headers={"Authorization": f"Bearer {os.environ['AIRPLAYCHART_KEY']}"},
    timeout=10,
)
res.raise_for_status()
body = res.json()
Example response
{
  "data": [
    {
      "code": "BR",
      "stations": 1210,
      "lastPlayAt": "2026-10-27T14:03:22.000Z",
      "url": "https://airplaychart.com/country/br"
    }
  ],
  "meta": {
    "nextCursor": "eyJvIjoxfQ"
  }
}