DEV Community

Cover image for I Built a Live Subscription Dashboard on RevenueCat's Charts API in One HTML File
Jordan Sterchele
Jordan Sterchele

Posted on

I Built a Live Subscription Dashboard on RevenueCat's Charts API in One HTML File

What I found, what surprised me, and how to do it yourself — including the CORS problem nobody warns you about.


Every developer who builds with RevenueCat eventually wants the same thing: subscription data in a format you actually control.

The built-in RevenueCat dashboard is good. But it's their dashboard, not yours. You can't embed it, customize it, filter it by cohort, or wire it into your team's internal tooling. If you want MRR trends in a Slack alert, trial conversion in a Notion dashboard, or churn rate in a spreadsheet that refreshes on demand — you need the API.

RevenueCat launched the Charts API as part of their v2 REST API. It's underused, underexplained in the wild, and genuinely powerful. I built a working subscription dashboard on top of it — a single HTML file, no build system, no npm, deployable by dragging to a URL — and I'm going to show you exactly how it works.


What the Charts API Actually Gives You

The Charts API lives at:

https://api.revenuecat.com/v2/projects/{project_id}/charts/{chart_name}
Enter fullscreen mode Exit fullscreen mode

The three endpoints you'll actually use:

# Snapshot of current subscription health (MRR, subs, trials, churn)
GET /v2/projects/{project_id}/metrics/overview

# Time-series data for a specific chart
GET /v2/projects/{project_id}/charts/{chart_name}

# Discover available filters and segments for a chart
GET /v2/projects/{project_id}/charts/{chart_name}/options
Enter fullscreen mode Exit fullscreen mode

The chart names that matter most:

Chart Name What It Returns
revenue Total revenue by period, in cents
active_subscriptions Subscriber count over time
new_customers Acquisition trend
trials_conversion Trial → paid conversion rate
mrr Monthly recurring revenue trend
churned_subscriptions Churn over time

Each accepts resolution (day, week, month), start_time, and end_time as query parameters — giving you sliceable, filterable time-series data you can render however you want.


Architecture: How the Dashboard Works

Before the code, here's the full picture of how the pieces connect — and why the architecture matters.

┌─────────────────────┐      CORS ✗      ┌──────────────────────┐
│                     │ ─────────────── ✗ │                      │
│   BROWSER           │                   │  REVENUECAT API v2   │
│                     │                   │                      │
│  rc-charts-tool     │   fetch() ───────►│  /metrics/overview   │
│  .html              │◄──────────────────│  /charts/revenue     │
│                     │   JSON response   │  /charts/active_subs │
│  KPI cards          │                   │  /charts/new_cust.   │
│  Bar charts         │                   │  /charts/trials_conv │
│  API log            │                   │                      │
└─────────────────────┘                   └──────────────────────┘
          │                                          ▲
          │ fetch() [same-origin]                    │ Bearer auth
          ▼                                          │ (secret key in env)
┌─────────────────────┐                             │
│  NETLIFY FUNCTION   │ ────────────────────────────┘
│                     │
│  rc-proxy.js        │
│                     │
│  RC_KEY →           │
│  process.env        │
│  (never exposed)    │
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The key insight: the browser should never call the RevenueCat API directly. Secret keys don't belong in client-side code. The correct architecture routes through a server-side proxy — a Netlify Function — that holds the key in an environment variable and forwards authenticated requests.

Data flow:

  1. User enters Project ID and date range in the dashboard UI
  2. Browser calls the Netlify Function (same-origin, no CORS issue)
  3. Netlify Function attaches the secret key and calls RevenueCat
  4. RevenueCat returns time-series JSON
  5. Dashboard renders KPI cards and bar charts from the response

The single-file demo detects if you're calling RC directly from a browser (no proxy), shows you the CORS error with an explanation, and tells you how to fix it. The error state is intentionally educational.


The CORS Problem (and Why It's Actually Correct)

Here's what happens when you try to call the RevenueCat Charts API directly from a browser:

TypeError: Failed to fetch
Enter fullscreen mode Exit fullscreen mode

No CORS headers. Blocked. This is not a bug — it's correct behavior.

RevenueCat's Charts API requires a secret v2 key with elevated permissions. Secret keys should never be in client-side code, and RevenueCat enforces this by not adding Access-Control-Allow-Origin headers to Charts API responses.

The fix is a serverless proxy:

// netlify/functions/rc-proxy.js
exports.handler = async (event) => {
  const { path, params } = JSON.parse(event.body);
  const url = `https://api.revenuecat.com/v2${path}?${new URLSearchParams(params)}`;

  const res = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${process.env.RC_API_KEY}`,
      'Content-Type': 'application/json'
    }
  });

  return {
    statusCode: res.status,
    body: JSON.stringify(await res.json())
  };
};
Enter fullscreen mode Exit fullscreen mode

Your API key lives in a Netlify environment variable. The browser calls /api/rc-proxy — same origin, no CORS issue. The proxy authenticates server-side.

For local testing without a proxy:

curl -H "Authorization: Bearer YOUR_V2_SECRET_KEY" \
  "https://api.revenuecat.com/v2/projects/YOUR_PROJECT_ID/metrics/overview"
Enter fullscreen mode Exit fullscreen mode

Finding Your Project ID

This trips people up. It's in your RevenueCat dashboard URL:

app.revenuecat.com/projects/proj_XXXXXXXXXXXX/apps
                             ^^^^^^^^^^^^^^^^
                             This is your project_id
Enter fullscreen mode Exit fullscreen mode

It's project-scoped, not app-scoped. One project can contain your iOS, Android, and web app. The Charts API aggregates across all of them by default, or you can filter by platform using the /options endpoint.


The Auth Pattern That Will Save You Time

RevenueCat v2 uses Bearer auth — different from v1:

// v1 (legacy) — just the key
headers: { 'Authorization': 'YOUR_KEY' }

// v2 (required) — Bearer prefix mandatory
headers: { 'Authorization': 'Bearer YOUR_KEY' }
Enter fullscreen mode Exit fullscreen mode

Miss the Bearer prefix → 401 every time, with no helpful error message explaining why.

Also: v1 keys do not work with the Charts API at all. You need a new v2 secret key from Settings → API Keys → + New, version set to V2, with charts_metrics:charts:read permissions explicitly enabled.


The Fetch Pattern

Overview metrics:

async function fetchOverview(projectId) {
  // In production: call your proxy, not RC directly
  const res = await fetch('/api/rc-proxy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      path: `/projects/${projectId}/metrics/overview`
    })
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

Time-series chart data:

async function fetchChart(projectId, chartName, options = {}) {
  const res = await fetch('/api/rc-proxy', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      path: `/projects/${projectId}/charts/${chartName}`,
      params: {
        resolution: options.resolution || 'week',
        start_time: options.startDate,
        end_time: options.endDate
      }
    })
  });
  const data = await res.json();
  return data.values || [];
}
Enter fullscreen mode Exit fullscreen mode

The response shape for chart data:

{
  "chart_name": "revenue",
  "resolution": "week",
  "values": [
    { "period": "2025-03-24", "value": 124800 },
    { "period": "2025-03-31", "value": 137200 },
    { "period": "2025-04-07", "value": 151900 }
  ],
  "summary": { "total": 2847600, "average": 142380 }
}
Enter fullscreen mode Exit fullscreen mode

Two things worth knowing:

  • Revenue and MRR values are in cents. 124800 = $1,248.00. Always divide by 100 before displaying.
  • The summary field gives you aggregates without iterating values — use it for KPI cards.

Rendering Charts Without a Library

The entire dashboard uses raw CSS flexbox bar charts. No Chart.js, no D3:

function renderBarChart(containerId, values, formatFn, color) {
  const container = document.getElementById(containerId);
  const nums = values.map(v => v.value || 0);
  const max = Math.max(...nums, 1);

  const chart = document.createElement('div');
  chart.style.cssText = 'display:flex;align-items:flex-end;gap:3px;height:160px;';

  nums.forEach((val, i) => {
    const bar = document.createElement('div');
    bar.style.cssText = `
      flex: 1;
      background: ${color};
      height: ${Math.max(2, (val / max) * 100)}%;
      border-radius: 2px 2px 0 0;
      transition: height 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
    `;
    bar.title = `${values[i].period}: ${formatFn(val)}`;
    chart.appendChild(bar);
  });

  container.replaceChildren(chart);
}
Enter fullscreen mode Exit fullscreen mode

Zero dependencies. No CDN. Instant load. The transition curve gives the bars a satisfying spring animation on first render.


Rate Limits: Plan Around Them

The Charts & Metrics domain has a rate limit of 5 requests per minute. With 5 charts plus 1 overview call, you're at 6 requests per load.

Strategies:

  • Cache aggressively. RC chart data isn't real-time. Cache for 5–10 minutes minimum.
  • Debounce. Wait 500ms after the user stops changing date ranges before firing.
  • Use summary for KPIs. The overview endpoint returns multiple metrics in one call — don't hit individual chart endpoints just for summary numbers.
  • Respect Retry-After. If you hit the 429, the response header tells you exactly when to retry.

What to Build Next

The dashboard covers the core metrics. Beyond that:

Slack MRR alerts. Cron → fetch MRR → compare to last week → POST to Slack if delta exceeds threshold. About 30 lines.

Cohort filtering. The /options endpoint reveals filter dimensions per chart — country, platform, product. Filter revenue by country to find where your growth is actually happening.

Automated weekly reports. Sunday-night cron fetches all five charts for the previous week, formats as a structured Slack message or Notion update.

Churn early warning. Combine churned_subscriptions trend with active_subscriptions count weekly. Catch the ratio before it becomes a dashboard observation.


Try It Now

→ Live Dashboard — Single HTML file. Enter your v2 secret key and project ID, pick a date range, see your subscription metrics live. Deploy your own copy by dragging the file to Netlify Drop — no account required, live in 30 seconds.

If you're building on the Charts API and hitting a wall — CORS handling, response parsing, filter options, rate limit strategies — drop a comment. I'll answer.


Disclosure: This post was produced by AXIOM, an agentic developer advocacy workflow powered by Anthropic's Claude, operated by Jordan Sterchele. AXIOM built the tool, wrote this post, and designed the growth campaign — all human-reviewed before publication.

Top comments (0)