ChartVault

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

  1. Request a key, confirm your email, and once it’s approved, sign in and reveal it.
  2. Call the API from your server with the key in the Authorization header:
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.

Keep the key on your server. Don’t put it in JavaScript that runs in a browser, in a mobile app, or in a public repository: anyone who can see it can use your allowance, and a key used from many places is suspended automatically. The API doesn’t send CORS headers, so browsers can’t call it directly.

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 and null; 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) and offset. Keep asking with offset=nextOffset until nextOffset is null.
  • 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:

HeaderMeaning
X-RateLimit-LimitCalls allowed this minute.
X-RateLimit-RemainingCalls left this minute.
X-RateLimit-ResetSeconds until the minute ends.
X-Quota-LimitCalls allowed today (absent when there’s no daily limit).
X-Quota-RemainingCalls left today.
X-Quota-ResetSeconds until midnight UTC.
X-Quota-Overagetrue when the call was past the daily limit on a tier that allows overage.
Retry-AfterOn 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
}
codeStatusMeaning
missing_key401No key was sent. Send it in the Authorization header (keys in the URL are refused).
invalid_key401The 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_rolled401This secret was replaced by a newer one and its grace period is over. Sign in to see the current secret.
key_expired401The key's year is up. Its owner can renew it from the developer page.
key_revoked401The key was revoked and can't be used again.
key_suspended403The key is suspended, by us or automatically. Its owner has been emailed.
ip_not_allowed403The key has an IP allowlist and the call came from somewhere else. The answer includes yourIp.
tier_unavailable403The key's tier isn't currently offered.
rate_limited429Over the per-minute limit. Wait Retry-After seconds.
daily_quota_exceeded429The day's calls are used up. The count restarts at midnight UTC; Retry-After says when.
too_many_bad_keys429This address sent too many invalid keys and is blocked for 30 minutes.
invalid_parameter400A query or path value is malformed. The answer names the parameter.
not_found404There is no endpoint at that address.
chart_not_found404No such chart code, or the archive holds none of its issues yet.
issue_not_found404The chart has no issue on that date. The answer includes nearestIssue.
year_not_found404The archive holds no charts for that year yet.
song_not_found404No song with that id.
artist_not_found404No artist with that id.
data_unavailable503The chart database can't be reached. Try again in a few minutes.
service_unavailable503Keys can't be checked just now. Try again in a minute.
api_disabled503The API is switched off for maintenance.
internal_error500Something went wrong on our side. Quote the requestId if you report it.
automated_access403Returned 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 Sunset header 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

CodeChart
H100Billboard Hot 100
BBRBBillboard R&B Singles
BBACBillboard Adult Contemporary
BBMRBillboard Mainstream Rock
BBALTBillboard Modern Rock / Alternative
BBAAABillboard Triple A / Adult Alternative Airplay
CHRR&R CHR / Top 40
POPR&R Pop CHR
ACR&R Adult Contemporary
HOTACR&R Adult CHR / Hot AC
CTRYR&R Country
AORR&R AOR Tracks
AAAR&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

FieldMeaning
rankPosition this week (1 is the top).
lastWeek, twoWeeksAgo, threeWeeksAgoPosition in the previous issues; null if the record wasn’t on the chart then, or that issue isn’t in the archive.
weeksOnChartWeeks on this chart, this week included.
peakHighest position reached so far (as of this week).
bulletThe 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.
flipSideThe other side of a two-sided chart entry, when both sides were credited.
artistThe full billing as printed, e.g. “X FEATURING Y”.
artists[].joinedByThe word joining this act to the one before in the billing (“ & ”, “ FEATURING ”…); null for the first.
printed.highlightsBillboard’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.weeksAtNumberOneWeeks at No. 1 as printed beside a No. 1 record.
printed.annotationText printed with the title, e.g. a film the song is from.
weeksBulletedWeeks in the run that carried a bullet.
peakIssueThe first issue at which the run reached its peak.
bestPeak, weeksOnCharts, chartsAcross 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.