Blog

How to Build a RevenueCat Dashboard (Without Building a Frontend)

10 min read

What you'll build

A live RevenueCat dashboard with five widgets:

  • MRR (KPI) — current monthly recurring revenue
  • Active subscribers (KPI) — current count, with active trials alongside
  • Revenue, last 28 days (KPI) — compared with the 28 days before
  • Subscription events (line chart) — new vs. cancelled per day, last 30 days
  • Top revenue products (table) — your highest-grossing SKUs

You'll write a few endpoints in your existing backend that wrap RevenueCat's REST API v2 and your own webhook data, then point dashboardbase at them. Total time: ~20 minutes if you already have RevenueCat webhooks set up.**

Why not just use RevenueCat's own dashboard?

RevenueCat's dashboard is genuinely good — far better than App Store Connect's own analytics. So why build another one?

Reasons mobile founders build a custom dashboard on top of RevenueCat:

  • Combine RevenueCat data with your own product data — e.g. "MRR vs. weekly active users vs. crash-free sessions"
  • Mobile-first viewing — you want these numbers in a native app on your phone, next to your own product metrics, one tap away
  • Share with co-founders or investors — without giving them RevenueCat seat access
  • Custom MRR definitions — exclude trials, exclude specific SKUs, weight by lifecycle stage
  • Combine iOS, Android, web subscriptions in one view with custom grouping

If none of those apply, just use RevenueCat's dashboard. If two or more apply, keep reading.

Prerequisites

  • A RevenueCat account with active subscriptions
  • A RevenueCat REST API v2 secret key (Project Settings → API keys)
  • A backend in any language (examples in Node.js)
  • A dashboardbase account

Step 1: The MRR endpoint **

The KPI widget contract:

response.json dashboardbase.com/s/your-company
{
  "title": "MRR",
  "actions": [
    {
      "title": "View Details",
      "type": "link",
      "url": "https://example.com/mrr-details"
    }
  ],
  "data": {
    "header": {
      "title": "$12,345",
      "subtitle": "vs last month",
      "badge": {
        "text": "+$1,120",
        "icon": "ArrowUp",
        "color": "Success"
      }
    }
  }
}

RevenueCat's REST API v2 has an overview endpoint that returns the same headline numbers as its dashboard. The response holds a metrics array: each entry has an id (such as mrr, active_subscriptions or active_trials) and a value.** One small helper turns that into a lookup:

// RevenueCat overview metrics: { metrics: [{ id, value, ... }] }
async function overview() {
  const response = await fetch(
    `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`,
    { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } }
  );
  if (!response.ok) throw new Error(`RevenueCat responded ${response.status}`);
  const { metrics } = await response.json();
  return (id) => metrics.find((m) => m.id === id)?.value ?? 0;
}

// GET /dashboards/revenuecat/mrr
app.get('/dashboards/revenuecat/mrr', async (req, res) => {
  const metric = await overview();
  const mrr = metric('mrr');
  const subscribers = metric('active_subscriptions');

  res.json({
    title: 'MRR',
    actions: [
      { title: 'View Details', type: 'link', url: 'https://app.revenuecat.com' },
    ],
    data: {
      header: {
        title: `$${Math.round(mrr).toLocaleString('en-US')}`,
        subtitle: 'Current MRR',
        badge: {
          text: `${subscribers.toLocaleString('en-US')} subscribers`,
          color: 'Blue',
        },
      },
    },
  });
});

Note: the overview endpoint returns current values, not last month's.** For a month-over-month arrow, store one MRR snapshot a day in your own database and compare against the row from 30 days ago, or use the revenue comparison in Step 3, which RevenueCat can answer directly.

You don't have to hand-write the remaining four. Two things make this faster:

  • Generating the endpoint. The free dashboardbase Skill teaches Claude Code, Cursor or any skills-capable agent the full JSON contract, so "wrap RevenueCat's revenue for the last 28 days as a KPI endpoint" comes back in the right shape rather than an invented one. No Skill install? The widget editor generates a copy-pasteable prompt — per widget, or one covering the whole dashboard — for whatever AI tool you use. It's a draft you review and run yourself.
  • Checking it. Paste the response into the endpoint validator and it says whether that widget type can render your JSON, with a live preview. It's public — no account required — so it's also the fastest way to sanity-check a shape before you deploy.

Step 2: Active subscribers KPI **

Same overview call, different metrics:

app.get('/dashboards/revenuecat/active-subscribers', async (req, res) => {
  const metric = await overview();
  const active = metric('active_subscriptions');
  const trials = metric('active_trials');

  res.json({
    title: 'Active subscribers',
    data: {
      header: {
        title: active.toLocaleString('en-US'),
        subtitle: 'Paying right now',
        badge: {
          text: `${trials.toLocaleString('en-US')} in trial`,
          color: 'Blue',
        },
      },
    },
  });
});

Step 3: Revenue, last 28 days **

RevenueCat's revenue endpoint takes a date range and returns one total, so comparing two windows is two calls.** Ending at yesterday avoids a partial day, which RevenueCat warns the most recent day can be:**

async function revenue(start, end) {
  const params = new URLSearchParams({ start_date: start, end_date: end });
  const response = await fetch(
    `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/revenue?${params}`,
    { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } }
  );
  if (!response.ok) throw new Error(`RevenueCat responded ${response.status}`);
  return (await response.json()).value;
}

const day = (offset) => new Date(Date.now() - offset * 86400000).toISOString().slice(0, 10);

app.get('/dashboards/revenuecat/revenue', async (req, res) => {
  const current = await revenue(day(28), day(1));
  const previous = await revenue(day(56), day(29));
  const change = previous > 0 ? Math.round(((current - previous) / previous) * 100) : 0;
  const up = change >= 0;

  res.json({
    title: 'Revenue',
    actions: [
      { title: 'View Details', type: 'link', url: 'https://app.revenuecat.com' },
    ],
    data: {
      header: {
        title: `$${Math.round(current).toLocaleString('en-US')}`,
        subtitle: 'Last 28 days',
        badge: {
          text: `${up ? '+' : ''}${change}%`,
          icon: up ? 'ArrowUp' : 'ArrowDown',
          color: up ? 'Success' : 'Danger',
        },
      },
    },
  });
});

The endpoint also accepts a revenue_type parameter: gross revenue (the default), revenue_net_of_taxes, or proceeds, which is what you keep after taxes and store commission.** Pick one deliberately; it's the difference between the number on the dashboard and the number in your bank.

Revenue by platform? RevenueCat's chart endpoint can segment charts such as mrr or revenue by dimensions like store.** Which segments a chart supports varies, so call its /charts/{chart_name}/options endpoint first and build the bar chart from what it returns. The bar chart contract is the same labels plus datasets shape as every other chart.

Step 4: Subscription events over time

The line chart needs new vs. cancelled subscriptions per day. The cleanest source for this is your own database, populated by RevenueCat webhooks. If you're not consuming RevenueCat webhooks yet, set that up first — it's the foundation for almost any custom analytics work on top of RevenueCat.

app.get('/dashboards/revenuecat/subscription-events', async (req, res) => {
  const days = parseInt(req.query.days) || 30;
  const since = subDays(new Date(), days);

  const events = await db.revenuecatEvents.findMany({
    where: {
      type: { in: ['INITIAL_PURCHASE', 'CANCELLATION'] },
      createdAt: { gte: since },
    },
  });

  // Bucket by day
  const buckets = {};
  for (const event of events) {
    const day = event.createdAt.toISOString().slice(0, 10);
    if (!buckets[day]) buckets[day] = { new: 0, cancelled: 0 };
    if (event.type === 'INITIAL_PURCHASE') buckets[day].new++;
    else buckets[day].cancelled++;
  }

  const days_sorted = Object.keys(buckets).sort();
  const totalNew = days_sorted.reduce((sum, d) => sum + buckets[d].new, 0);

  res.json({
    title: 'Subscription events',
    data: {
      header: {
        title: totalNew.toLocaleString('en-US'),
        subtitle: `New subscriptions, last ${days} days`,
      },
      labels: days_sorted,
      datasets: [
        {
          data: days_sorted.map(d => ({ value: buckets[d].new })),
          label: 'New',
        },
        {
          data: days_sorted.map(d => ({ value: buckets[d].cancelled })),
          label: 'Cancelled',
        },
      ],
    },
  });
});

This endpoint accepts a days query param so it works with dashboardbase's date range selector.

Step 5: Top revenue products (table)

Like Step 4, this reads from your own database, filled by RevenueCat webhooks. It assumes you store each purchase and renewal event's product ID, store and USD price when the webhook arrives; adjust the names to your schema:

app.get('/dashboards/revenuecat/top-products', async (req, res) => {
  const rows = await db.$queryRaw`
    SELECT product_id, store, sum(price_usd) AS revenue, count(*) AS purchases
    FROM revenuecat_events
    WHERE type IN ('INITIAL_PURCHASE', 'RENEWAL')
      AND created_at >= now() - interval '30 days'
    GROUP BY product_id, store
    ORDER BY revenue DESC
    LIMIT 10
  `;

  res.json({
    title: 'Top revenue products',
    actions: [
      { title: 'View All', type: 'link', url: 'https://example.com/products' },
    ],
    data: {
      columns: [
        { text: 'Product', width: 40 },
        { text: 'Platform', width: 20 },
        { text: 'Revenue (30d)', width: 25 },
        { text: 'Purchases', width: 15 },
      ],
      rows: rows.map((row) => {
        const ios = row.store === 'APP_STORE';
        return [
          { text: row.product_id },
          {
            text: ios ? 'iOS' : 'Android',
            badge: { text: ios ? 'iOS' : 'Android', color: ios ? 'Dark' : 'Success' },
          },
          { text: `$${Math.round(Number(row.revenue)).toLocaleString('en-US')}` },
          { text: String(row.purchases) },
        ];
      }),
    },
  });
});

Step 6: Wire it up in dashboardbase

  1. Sign in, create a new dashboard, name it "RevenueCat."
  2. Click Add datasource and paste the URL of your first endpoint. Your workspace's endpoint secret is already attached — it shows as a read-only row above your own headers. Your RevenueCat key never goes in here; it stays in your backend.
  3. Test the datasource — JSON response shows up.
  4. Drop a KPI widget on the grid, point it at the MRR datasource.
  5. Repeat for the other four widgets.
  6. Set refresh interval to 5 minutes. Publish.
  7. Open it on your phone via the dashboardbase iOS or Android app.

That's the dashboard you'd otherwise have built a separate React Native app to view.

The mobile founder angle

If you're shipping a mobile app, you check your numbers from your phone. That's not a nice-to-have — it's how you actually run the business between meetings, on the train, in line at coffee.

The combination here genuinely matters:

  • RevenueCat owns the source-of-truth subscription data
  • Your backend wraps it into the exact shape you want
  • dashboardbase renders it natively on your phone with push notifications

You can get something close to this with RevenueCat's own dashboard on mobile web, but it'll never feel as good as a native widget you can deep-link into from a notification.

Securing the endpoints

These endpoints expose subscription revenue. Two secrets are in play and it's worth keeping them straight:

  • Your RevenueCat key authenticates you to RevenueCat. It lives in your backend and never leaves it.
  • Your dashboardbase endpoint secret authenticates dashboardbase to your endpoint.

That second one is the one you configure here, and it needs almost no work. Every workspace has a single endpoint secret, and dashboardbase sends it as an x-dashboardbase-secret header on every request. You compare one string:

const SECRET = process.env.DASHBOARDBASE_ENDPOINT_SECRET

// Fail at boot rather than serving subscriber data unprotected.
if (!SECRET) throw new Error('DASHBOARDBASE_ENDPOINT_SECRET is not set')

app.use('/dashboard', (req, res, next) => {
  if (req.get('x-dashboardbase-secret') !== SECRET) {
    return res.status(401).json({ error: 'unauthorized' })
  }
  next()
})

Find the value under Settings → Secrets → Endpoint Secret (or /workspace), masked with a reveal toggle and the environment variable name beside it. Rotating it keeps the old value valid for 24 hours so nothing breaks mid-deploy.

Prefer something you already have? Custom headers and Basic Auth are both supported and stack on top of the secret rather than replacing it.

And the validator checks reality, not intent. Testing a datasource fires one extra request with every credential stripped, then reports what came back — a green "Protected — returns 401 without the secret", or a yellow warning naming the header to look at. It won't stop you saving; it just won't let an open endpoint look closed.

Otherwise: no tokens in the URL, HTTPS only.

Common gotchas

  • Trial users. RevenueCat counts trials as "active subscribers" by default. Decide whether you want them in your dashboard's MRR.
  • Refunds and chargebacks. If you compute revenue yourself from webhook events, subtract refunds explicitly. Check RevenueCat's metric definitions before assuming what their numbers include.**
  • Currency conversion. Multi-currency apps need to pick a reporting currency. Pick one in your endpoint and stay consistent.
  • iOS vs. Android revenue isn't apples-to-apples. Apple takes 30% (or 15% for small developers), Google's similar. Decide whether your dashboard shows gross or net.
  • API rate limits. RevenueCat's Charts & Metrics endpoints share a default limit of 25 requests per minute per project.** Cache responses for a few minutes in your endpoint; dashboardbase polls at your refresh interval, and several widgets on one dashboard add up.

Next steps

  • Add a churn rate gauge widget.
  • Add a "trial-to-paid conversion" KPI. The mobile app KPI dashboard covers which app metrics deserve a tile.
  • Add App Store downloads and proceeds from the App Store Connect API.
  • Add a cohort retention table (a heavier endpoint, but doable).
  • Add a webhook trigger so your dashboard pings you when MRR crosses a milestone.

Each one is another small endpoint. The dashboard grows; the architecture stays simple.

About the third-party code **

The RevenueCat examples on this page were checked against RevenueCat's public REST API v2 documentation on 6 October 2026. Third-party APIs change: endpoints, metric IDs, field names and rate limits can be renamed or retired after we publish. Treat this code as inspiration and a starting point, and check it against RevenueCat's current API reference before you rely on it.

Your first dashboard in ten minutes.

Return JSON from an endpoint you already own. We handle the charts, hosting, sharing and the mobile app — free for 14 days.