ChartVault API · version 1
API documentation
Everything you need to call the API: authentication, limits, errors, every endpoint and every field. Machine-readable: OpenAPI 3.1 description.
Quick start
- Request a key, confirm your email, and once it’s approved, sign in and reveal it.
- Call the API from your server with the key in the
Authorizationheader:
curl -H "Authorization: Bearer $CHARTVAULT_KEY" \ "https://www.chartvault.fm/api/v1/charts/H100/issues/1985-07-13"
The base address is https://www.chartvault.fm/api/v1. Every endpoint is a GET and answers in JSON (UTF-8). Add ?pretty=1 to any call for indented output while you explore.
Authentication
Every endpoint except /api/v1 itself needs a key. Send it in a header, either
Authorization: Bearer cv_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
or X-API-Key: cv_live_…. Keys sent in the URL (?api_key=) are refused, because URLs end up in logs and browser histories. Keys start with cv_live_ followed by 32 letters and digits.
Responses
A successful call answers 200 OK with an object holding data (the answer) and meta (about the answer):
{
"data": { ... } or [ ... ],
"meta": {
"total": 412, "limit": 100, "offset": 0, "nextOffset": 100, // lists only
"attribution": "Chart data from ChartVault (www.chartvault.fm)",
"terms": "https://www.chartvault.fm/api/terms"
}
}
- Fields are
camelCase. A field with no value is present andnull; fields are never left out. - Dates are
YYYY-MM-DD(issue dates as printed: Billboard dated issues on Saturdays, Radio & Records on Fridays). Times are UTC, ISO 8601. - Titles and names are given as the archive holds them, which for most charts is the magazine’s capitals.
- Lists take
limit(1–1000, default 100) andoffset. Keep asking withoffset=nextOffsetuntilnextOffsetisnull. - Answers may be cached by your client for five minutes (
Cache-Control: private, max-age=300). The archive changes only when new data is loaded.
Limits
Each key has a per-minute limit and, on most tiers, a daily limit (see tiers). A minute is a clock minute; a day is a UTC day, starting at 00:00 UTC. Every answer tells you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Calls allowed this minute. |
X-RateLimit-Remaining | Calls left this minute. |
X-RateLimit-Reset | Seconds until the minute ends. |
X-Quota-Limit | Calls allowed today (absent when there’s no daily limit). |
X-Quota-Remaining | Calls left today. |
X-Quota-Reset | Seconds until midnight UTC. |
X-Quota-Overage | true when the call was past the daily limit on a tier that allows overage. |
Retry-After | On a 429: seconds to wait before trying again. |
Over a limit you get 429 Too Many Requests. Wait for Retry-After and slow down; don’t retry in a tight loop. A key that keeps going after being refused (more than about a thousand 429s in an hour) is suspended automatically. Refused calls don’t count against your daily allowance. Every call that gets past the key check and limits counts, including ones that answer 400 or 404.
Errors
Errors use the standard problem details format (RFC 9457), with Content-Type: application/problem+json. Branch on code, which never changes; title and detail are for people and may be reworded.
{
"type": "https://www.chartvault.fm/api/docs#error-rate_limited",
"title": "Too many requests",
"status": 429,
"detail": "This key may make 60 calls a minute. Wait 21 seconds (the Retry-After header) and try again.",
"code": "rate_limited",
"instance": "/api/v1/years",
"requestId": "0HNPFL61ETV9E:00000001",
"retryAfter": 21
}
| code | Status | Meaning |
|---|---|---|
missing_key | 401 | No key was sent. Send it in the Authorization header (keys in the URL are refused). |
invalid_key | 401 | The key isn't one we issued, or it was copied incompletely. 30 invalid keys from one address in 10 minutes shuts that address out for 30 minutes. |
key_rolled | 401 | This secret was replaced by a newer one and its grace period is over. Sign in to see the current secret. |
key_expired | 401 | The key's year is up. Its owner can renew it from the developer page. |
key_revoked | 401 | The key was revoked and can't be used again. |
key_suspended | 403 | The key is suspended, by us or automatically. Its owner has been emailed. |
ip_not_allowed | 403 | The key has an IP allowlist and the call came from somewhere else. The answer includes yourIp. |
tier_unavailable | 403 | The key's tier isn't currently offered. |
rate_limited | 429 | Over the per-minute limit. Wait Retry-After seconds. |
daily_quota_exceeded | 429 | The day's calls are used up. The count restarts at midnight UTC; Retry-After says when. |
too_many_bad_keys | 429 | This address sent too many invalid keys and is blocked for 30 minutes. |
invalid_parameter | 400 | A query or path value is malformed. The answer names the parameter. |
not_found | 404 | There is no endpoint at that address. |
chart_not_found | 404 | No such chart code, or the archive holds none of its issues yet. |
issue_not_found | 404 | The chart has no issue on that date. The answer includes nearestIssue. |
year_not_found | 404 | The archive holds no charts for that year yet. |
song_not_found | 404 | No song with that id. |
artist_not_found | 404 | No artist with that id. |
data_unavailable | 503 | The chart database can't be reached. Try again in a few minutes. |
service_unavailable | 503 | Keys can't be checked just now. Try again in a minute. |
api_disabled | 503 | The API is switched off for maintenance. |
internal_error | 500 | Something went wrong on our side. Quote the requestId if you report it. |
automated_access | 403 | Returned to AI crawlers and scrapers that ask for the archive's web pages instead of using the API. |
Looking after a key
- Expiry. Keys last 12 months. We email the owner 30 and 7 days before; renew with one click on the developer page (from 30 days before expiry). The key itself doesn’t change.
- Rolling. If a key may have been exposed, roll it on the developer page. You get a new secret at once and the old one keeps working for 7 days, so you can switch without an outage. Calls with the old secret carry a
Sunsetheader saying when it stops. - IP allowlist. Restrict a key to your servers’ addresses (single addresses or ranges such as
203.0.113.0/24). A stolen key is then useless elsewhere. - Automatic suspension. A key is suspended, and its owner emailed, if it is used from more than 25 addresses in a day (without an allowlist), keeps calling after being rate-limited, or is repeatedly used from outside its allowlist.
- Check a key’s tier, limits, usage and expiry at any time with
/me.
Endpoints
GET/api/v1/charts
The charts the archive holds, with the years each covers. meta.archiveFirstYear and meta.archiveLastYear give the span of the whole archive.
{ "data": [ { "code": "H100", "name": "Billboard Hot 100", "publisher": "Billboard", "firstYear": 1958, "lastYear": 2026 }, ... ] }
GET/api/v1/charts/{chart}
One chart, with namesInPrint: what the magazine called it through its history. from is the first issue with that name (null: from the start); until is the first issue with the next name (null: still current).
{ "data": { "code": "BBMR", "name": "Billboard Mainstream Rock", "publisher": "Billboard", "firstYear": 1981, "lastYear": 2026,
"namesInPrint": [ { "from": null, "until": "1984-09-15", "name": "Top Tracks" },
{ "from": "1984-09-15", "until": "1986-04-12", "name": "Top Rock Tracks" }, ... ] } }
GET/api/v1/charts/{chart}/issues?year={year}
The chart’s issue dates, oldest first: one year’s with year, or the whole run without it.
{ "data": [ "1985-01-05", "1985-01-12", ... ], "meta": { "chart": "H100", "year": 1985, "count": 52 } }
GET/api/v1/charts/{chart}/issues/{date}
A whole chart week. date must be an issue date; if it isn’t, the 404 names the nearestIssue. chartName is the name the chart carried that week. note explains anything unusual about the week (for example a week rebuilt from the next issue). previousIssue and nextIssue step through the run.
{ "data": {
"chart": "H100", "chartName": "The Billboard Hot 100", "publisher": "Billboard", "issueDate": "1985-07-13",
"positions": 100, "previousIssue": "1985-07-06", "nextIssue": "1985-07-20", "note": null,
"entries": [ {
"rank": 1, "lastWeek": 2, "twoWeeksAgo": 4, "threeWeeksAgo": 7, "weeksOnChart": 12, "peak": 1,
"bullet": true, "status": null,
"song": { "id": 4127, "title": "...", "flipSide": null },
"artist": "...",
"artists": [ { "id": 812, "name": "...", "joinedBy": null } ],
"printed": { "lastWeek": "2", "twoWeeksAgo": "4", "weeksOnChart": 12, "title": "...", "artist": "...",
"label": "...", "annotation": null, "highlights": [ "NO1" ], "weeksAtNumberOne": 1 }
}, ... ] } }
printed is Billboard only: the figures and wording as the magazine printed them that week, where they differ from (or add to) the archive’s computed ones. It is null for weeks without a transcription and for Radio & Records charts.
GET/api/v1/years
Every year in the archive’s span, and whether it has been loaded yet. note explains a year that isn’t.
{ "data": [ { "year": 1958, "loaded": true, "note": null }, ... ], "meta": { "count": 69, "loaded": 36 } }
GET/api/v1/years/{year}
The charts published in a year, with their names that year and their first and last issues.
{ "data": [ { "chart": "CHR", "chartName": "R&R CHR / Top 40", "publisher": "Radio & Records", "issues": 52,
"firstIssue": "1989-01-06", "lastIssue": "1989-12-29" }, ... ] }
GET/api/v1/songs?letter={letter} | ?q={text}
The song index. Give letter (one letter A–Z, or # written %23 for titles starting with a digit or symbol) or q (2–80 characters, matched against titles and artists). Paged with limit and offset.
{ "data": [ { "id": 4127, "title": "...", "artist": "...", "firstYear": 1985, "bestPeak": 1, "weeksOnCharts": 31, "charts": 3 }, ... ],
"meta": { "total": 412, "limit": 100, "offset": 0, "nextOffset": 100, "letter": "B", "q": null } }
GET/api/v1/songs/{id}?include=history
One record: its credited acts and its run on every chart it made. With include=history, each run also lists every week’s position.
{ "data": { "id": 4127, "title": "...", "artist": "...", "artists": [ { "id": 812, "name": "...", "joinedBy": null } ],
"charts": 3, "weeksOnCharts": 31, "bestPeak": 1,
"chartRuns": [ { "chart": "H100", "chartName": "The Billboard Hot 100", "weeks": 20, "peak": 1, "weeksBulleted": 9,
"firstIssue": "1985-04-20", "lastIssue": "1985-08-31", "peakIssue": "1985-07-13", "flipSide": null,
"history": [ { "issueDate": "1985-04-20", "rank": 78, "bullet": true }, ... ] }, ... ] } }
GET/api/v1/artists?letter={letter} | ?q={text}
The artist index, as for songs. songs is how many charted records the act is credited on.
{ "data": [ { "id": 812, "name": "...", "songs": 14 }, ... ], "meta": { "total": 96, ... } }
GET/api/v1/artists/{id}
One act and every record it is credited on, each with its chart runs. artist is the record’s full billing; billingOrder is where this act appears in it (1 = first named).
{ "data": { "id": 812, "name": "...", "songs": [ { "id": 4127, "title": "...", "artist": "...", "billingOrder": 1, "firstYear": 1985,
"chartRuns": [ { "chart": "H100", "chartName": "The Billboard Hot 100", "weeks": 20, "peak": 1,
"firstIssue": "1985-04-20", "lastIssue": "1985-08-31" } ] }, ... ] } }
GET/api/v1/me
The key you’re calling with: its tier, limits, today’s usage and expiry. Counts as a call.
{ "data": { "key": { "id": 7, "label": "My project", "prefix": "cv_live_Ab3dEf", "created": "2026-10-21T19:42:25Z",
"expires": "2027-10-21T19:42:25Z", "ipAllowlist": [] },
"tier": { "code": "FREE", "name": "Free", "commercialUse": false, "aiTraining": false },
"limits": { "perMinute": 60, "perDay": 10000, "overageAllowed": false },
"usage": { "today": 132, "remainingToday": 9868, "resetsInSeconds": 15840 } } }
Chart codes
| Code | Chart |
|---|---|
H100 | Billboard Hot 100 |
BBRB | Billboard R&B Singles |
BBAC | Billboard Adult Contemporary |
BBMR | Billboard Mainstream Rock |
BBALT | Billboard Modern Rock / Alternative |
BBAAA | Billboard Triple A / Adult Alternative Airplay |
CHR | R&R CHR / Top 40 |
POP | R&R Pop CHR |
AC | R&R Adult Contemporary |
HOTAC | R&R Adult CHR / Hot AC |
CTRY | R&R Country |
AOR | R&R AOR Tracks |
AAA | R&R Adult Alternative / Triple A |
Which of these hold data, and for which years, is answered by /charts. Names changed over the years; see namesInPrint.
Field glossary
| Field | Meaning |
|---|---|
rank | Position this week (1 is the top). |
lastWeek, twoWeeksAgo, threeWeeksAgo | Position in the previous issues; null if the record wasn’t on the chart then, or that issue isn’t in the archive. |
weeksOnChart | Weeks on this chart, this week included. |
peak | Highest position reached so far (as of this week). |
bullet | The record carried a bullet (the magazine’s mark for strong upward movement). |
status | "debut" (first week on the chart), "re-entry" (back after dropping off), or null. |
flipSide | The other side of a two-sided chart entry, when both sides were credited. |
artist | The full billing as printed, e.g. “X FEATURING Y”. |
artists[].joinedBy | The word joining this act to the one before in the billing (“ & ”, “ FEATURING ”…); null for the first. |
printed.highlights | Billboard’s banners that week: NO1 No. 1, HSD Hot Shot Debut, PPS/PPA Power Pick sales/airplay, HMS/HMA Hot Mover sales/airplay (1986), GGS/GGA/GGD Greatest Gainer sales/airplay/digital. |
printed.weeksAtNumberOne | Weeks at No. 1 as printed beside a No. 1 record. |
printed.annotation | Text printed with the title, e.g. a film the song is from. |
weeksBulleted | Weeks in the run that carried a bullet. |
peakIssue | The first issue at which the run reached its peak. |
bestPeak, weeksOnCharts, charts | Across all charts: best position, total weeks, and how many charts the record made. |
Code examples
curl
curl -s -H "Authorization: Bearer $CHARTVAULT_KEY" \ "https://www.chartvault.fm/api/v1/songs?q=hold%20on&limit=20&pretty=1"
Python (requests)
import os, time, requests
API = "https://www.chartvault.fm/api/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["CHARTVAULT_KEY"]
def get(path, **params):
while True:
r = session.get(API + path, params=params, timeout=30)
if r.status_code == 429: # over a limit: wait as told
time.sleep(int(r.headers.get("Retry-After", "60")))
continue
if r.status_code >= 400:
problem = r.json()
raise RuntimeError(f"{problem['code']}: {problem['detail']}")
return r.json()
week = get("/charts/H100/issues/1985-07-13")["data"]
for e in week["entries"][:10]:
print(e["rank"], e["song"]["title"], "-", e["artist"])
# every song beginning with B, page by page
offset = 0
while offset is not None:
page = get("/songs", letter="B", limit=1000, offset=offset)
for s in page["data"]:
...
offset = page["meta"]["nextOffset"]
JavaScript (Node 18+)
const API = "https://www.chartvault.fm/api/v1";
async function get(path) {
for (;;) {
const r = await fetch(API + path, { headers: { Authorization: `Bearer ${process.env.CHARTVAULT_KEY}` } });
if (r.status === 429) {
await new Promise(ok => setTimeout(ok, 1000 * Number(r.headers.get("retry-after") ?? 60)));
continue;
}
const body = await r.json();
if (!r.ok) throw new Error(`${body.code}: ${body.detail}`);
return body;
}
}
const { data: song } = await get("/songs/4127?include=history");
for (const run of song.chartRuns) console.log(run.chartName, "peak", run.peak, "for", run.weeks, "weeks");
Changes
Within v1, fields and endpoints may be added but are never removed or renamed, and code values never change. Anything incompatible would come as v2, with notice to key holders by email. Credit ChartVault wherever the data is shown, as the terms require; the wording is in every answer’s meta.attribution.
Questions or a problem? Use the contact page and include the requestId from any error.