ELEMENT/59
Log inStart free →
◆ Developers · API

Public API.

Read-only HTTP endpoints for our weekly charts, public artist profiles, and a drop-in JavaScript embed. No authentication, no tokens, no signup. Just call the URLs.

Status: v1 (implicit). Breaking changes ship under/api/v2/* with Deprecation and Sunsetheaders over 6 months. The canonical home for these docs will eventually move to a dedicated docs site — this page is the source of truth until then.

1. Authentication

None. Every endpoint documented here is fully public. There is no API key, no bearer token, no OAuth. Cross-origin browser requests are allowed (see CORS below).

We may introduce optional bearer tokens later for partners who want higher rate-limit ceilings. When that ships, the same endpoints will continue to work without auth at the documented limits — the token will only ever raise the cap, never make an existing call fail.

2. CORS posture

Public GETs send Access-Control-Allow-Origin: * and Vary: Origin. OPTIONS preflight is handled where any non-simple header (e.g. a custom Content-Type) is sent.

POST endpoints elsewhere on the site (newsletter signup, DMCA intake, content reports) use a strict Origin allowlist; cross-origin POSTs return 403. Those endpoints are not part of the public API.

3. Rate limits

Every public endpoint is rate-limited per source IP. Exceeding the limit returns 429 Too Many Requests with a Retry-After header and a structured body (see Error shape). Every response — including 200s — carries the X-RateLimit-* headers so you can pace yourself.

EndpointPolicyLimitWindow
GET /api/charts/[genre]chartPublic60 reqper minute / IP
GET /api/public/charts/[genre]chartEmbed300 reqper minute / IP
GET /api/artists/[handle]chartPublic60 reqper minute / IP
GET /embed/charts/[genre]/embed.jscached, 1-week immutablen/acached at the edge

Response headers (every call)

X-RateLimit-Limit:     60
X-RateLimit-Remaining: 59
X-RateLimit-Reset:     1714329600   # unix seconds
Retry-After:           30           # only on 429

The default policy is the higher-throughput route; the embed JSON endpoint at /api/public/charts/[genre] is the one we recommend for third-party traffic. The internal /api/charts/[genre] exists for first-party callers.

4. Error shape

Every non-2xx response returns this exact JSON shape. Never plain-text, never HTML, never a stack trace.

{
  "code": "rate_limited",
  "message": "Too many requests. Retry after 30s."
}
StatuscodeWhen
404not_foundGenre or handle does not exist (or is non-public).
429rate_limitedSource IP exceeded the bucket. Honour Retry-After.
403forbiddenIP appears on the abuse blocklist.

5. GET /api/charts/[genre]

The first-party chart JSON. Returned from a snapshot computed every Friday at 00:00 UTC. Cached at the edge for 60 seconds with a 5-minute stale-while-revalidate window.

Genre: kebab-case slug, e.g. ambient, house, electronic. The full list is available via the marketing site at /charts.

Response 200

{
  "genre": "Ambient",
  "genreSlug": "ambient",
  "weekNumber": 17,
  "year": 2026,
  "snapshotId": "ckxxxxxxxxxxx",
  "snapshotAt": "2026-04-25T00:00:00.000Z",
  "weights": { "plays": 0.6, "saves": 0.25, "completion": 0.15 },
  "tracks": [
    {
      "rank": 1,
      "id": "trk_xxxxxxxxxxx",
      "title": "Slow Light",
      "artist": "Iris Vale",
      "artistHandle": "iris-vale",
      "genre": "Ambient",
      "plays": 12480,
      "movement": 3,
      "coverUrl": null,
      "duration": 248,
      "components": { "plays": 0.62, "saves": 0.21, "completion": 0.17 }
    }
  ]
}

Response headers

Cache-Control: public, max-age=60, s-maxage=60, stale-while-revalidate=300
X-Chart-Snapshot-Id: ckxxxxxxxxxxx
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1714329600

curl

curl -i https://element59.app/api/charts/ambient

6. GET /api/public/charts/[genre]

Same data shape as /api/charts/[genre], but tuned for third-party traffic: explicit CORS, longer cache, and a higher rate-limit ceiling (300 rpm/IP). Use this endpoint when calling from another origin.

Response headers

Cache-Control: public, s-maxage=300, stale-while-revalidate=86400
Access-Control-Allow-Origin: *
Vary: Origin
X-Chart-Snapshot-Id: ckxxxxxxxxxxx
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 299
X-RateLimit-Reset: 1714329600

curl

curl -i https://element59.app/api/public/charts/ambient

Browser fetch

const r = await fetch(
  "https://element59.app/api/public/charts/ambient",
  { headers: { Accept: "application/json" } },
);
const chart = await r.json();

7. GET /api/artists/[handle]

Public artist profile data. Returns 404 for any artist whose visibility is not public— we do not leak the existence of private accounts.

Handle: the artist handle as it appears in /artist/<handle> on the marketing site. Lowercase, kebab-case, max 64 chars.

Response 200

{
  "handle": "iris-vale",
  "name": "Iris Vale",
  "bio": "Ambient + slow techno. Berlin.",
  "pfpUrl": "https://cdn.element59.app/...",
  "coverUrl": "https://cdn.element59.app/...",
  "visibility": "public",
  "trackCount": 12,
  "totalPlays": 248120,
  "chartAppearances": 4,
  "socialLinks": [
    { "platform": "tiktok",  "url": "https://tiktok.com/@iris.vale" },
    { "platform": "youtube", "url": "https://youtube.com/@irisvale" }
  ],
  "releases": [
    {
      "id": "rel_xxxxxxxxxxx",
      "title": "Slow Light",
      "releasedAt": "2026-04-19T00:00:00.000Z",
      "coverUrl": null,
      "streamCount": 12480,
      "chartRankCurrent": 1
    }
  ]
}

Response headers

Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400
Access-Control-Allow-Origin: *
Vary: Origin
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1714329600

curl

curl -i https://element59.app/api/artists/iris-vale

8. Chart embed

Drop the top-10 chart for any genre into a third-party page with one script tag. The script injects a sandboxed <iframe>pointing at our embed widget — no NavBar, no Footer, dark by default with a prefers-color-scheme: light override.

Script tag (recommended)

<div id="e59-chart"></div>
<script
  src="https://element59.app/embed/charts/ambient/embed.js"
  async
></script>

The script looks for an element with id="e59-chart"; if none is present it falls back to the script tag's parent. The injected iframe carries a strict sandbox attribute (allow-scripts and allow-popups only — no allow-same-origin) so it can never reach your host page's cookies.

Iframe (alternative)

If you prefer to skip the script tag, embed the iframe directly:

<iframe
  src="https://element59.app/embed/charts/ambient"
  style="width:100%;max-width:640px;border:0;background:#0a0a0a"
  height="640"
  loading="lazy"
  title="ELEMENT/59 ambient chart"
></iframe>

The embed page sends Content-Security-Policy: frame-ancestors * so any origin can frame it. It is noindex— the canonical surface for an artist click-through is /charts/<genre>.

postMessage protocol (forward-compatible)

The script-tag loader listens for e59.resize events from the iframe and updates its height. Future events follow the same shape:

{ "source": "e59-embed", "type": "e59.ready",       "version": 1, "genre": "ambient", "weekNumber": 17, "year": 2026 }
{ "source": "e59-embed", "type": "e59.resize",      "version": 1, "height": 612 }
{ "source": "e59-embed", "type": "e59.track.click", "version": 1, "trackId": "trk_...", "artistHandle": "iris-vale", "rank": 1 }

9. Versioning & changes

The current shape is implicit v1. Backwards-incompatible changes ship under /api/v2/*. When v1 enters sunset, the existing endpoints will return:

Deprecation: true
Sunset: Sat, 31 Oct 2026 00:00:00 GMT
Link: </api/v2/charts/ambient>; rel="successor-version"

We commit to a 6-month minimum sunset window for any breaking change. Subscribe to the newsletter from the footer for advance notice.

Bug reports: hello@element59.com.