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
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.
Send it in a header
Every request carries the key in the
Authorizationheader. Keep it in an environment variable such asAIRPLAYCHART_KEY, not in your code.Authorization: Bearer ac_your_keyMake 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_keyNever 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 day | 10,000 per key, counted per UTC day |
|---|---|
| Requests per minute | 60 requests per key |
| Play history | 7 days back |
| Keys | Up to 3 active keys per account |
| Price | Free, 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.
| Form | Example | Means |
|---|---|---|
| ISO 8601 with a zone | 2026-10-27T12:00:00Z or 2026-10-27T09:00-03:00 | That instant |
| A date | 2026-10-27 | Midnight UTC at the start of that day |
| Epoch milliseconds | 1793102400000 | That 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"
}| Status | Code | Meaning | What to do |
|---|---|---|---|
| 301 | moved | The artist was merged into another. The body carries the new id; Location carries the new URL. | Follow Location and store the new id. |
| 400 | key_in_url | The 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. |
| 400 | bad_limit | limit is not a whole number of 1 or more. | Send 1 to 100, or leave it out for 50. |
| 400 | bad_cursor | cursor is not one this API issued. | Pass back a meta.nextCursor unchanged. |
| 400 | bad_time | since 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. |
| 400 | bad_range | since is after until. | Swap them. |
| 400 | bad_country | country is not a two-letter ISO 3166 code. | Send two letters, like BR. |
| 400 | bad_filter | More than one of country, genre and q on /v1/stations. | Keep one of them. |
| 400 | bad_query | q is missing, too short or too long. | Check the limits of q on that endpoint. |
| 400 | bad_type | type on /v1/search is not station, artist or song. | Send one of the three. |
| 401 | invalid_key | No key, or a key that is unknown or revoked. | Check the header, or make a new key on your account page. |
| 404 | not_found | No such id, week or path. | Check the id and the path. |
| 405 | method_not_allowed | The API only answers GET (and OPTIONS for CORS). | Use GET. |
| 410 | gone | The artist was removed. | Drop the id from your data. |
| 429 | rate_limited | Over 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. |
| 500 | internal_error | Something broke on our side. | Try again shortly. |
| 503 | api_paused | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
country | query | string | optional | Only stations in this country. | Two letters, ISO 3166, like BR. |
genre | query | string | optional | Only stations in this genre: an id from /v1/genres, main or sub. An unknown id gives an empty list. | None |
q | query | string | optional | Only stations whose name matches this text. | At most 100 characters. |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The station id. | None |
since | query | time | optional | Only plays at or after this time. | See Formats. Never reaches back more than 7 days. |
until | query | time | optional | Only plays at or before this time. | See Formats. Never reaches back more than 7 days. |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The station id. | None |
limit | query | integer | optional | How 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
weekStart | path | string | required | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
country | query | string | optional | One 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
limit | query | integer | optional | How 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
}
}GET/v1/search
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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
q | query | string | required | The text to search for. | At least 2 characters; longer than 80 is cut to 80. |
type | query | enum | required | What to search: station, artist or song. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The artist id. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The artist id. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The artist id. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The song id. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The song id. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
country | query | string | optional | Only playlists of stations in this country. | Two letters, ISO 3166, like BR. |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | integer | required | The playlist id. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | string | required | A genre id from /v1/genres, main or sub, like pop or dance-pop. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
id | path | string | required | A genre id from /v1/genres, main or sub, like pop or dance-pop. | None |
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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
| Name | In | Type | Required | Description | Default and limits |
|---|---|---|---|---|---|
limit | query | integer | optional | How many rows to return. | Default 50. A whole number from 1; above 100 is cut to 100. |
cursor | query | cursor | optional | The 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"
}
}