Self-reported metrics endpoint

How to expose your project's numbers with your own JSON endpoint. Using an AI agent? Plain Markdown version.

Quick spec

  • Method: GET, no auth
  • URL: https, on your project's own domain
  • Response: JSON array of 1 to 5 {"label": string, "value": number, "public"?: boolean} objects with current values
  • public defaults to false: private labels show blurred to everyone but you
  • To hide the numbers as well, turn on "Growth only" for a metric on your project page: visitors see the % change, not the values
  • We poll it every hour and record the time ourselves

Example response:

[
  { "label": "DAU", "value": 1234, "public": true },
  { "label": "Signups", "value": 56 }
]

URL

  • Must use https.
  • Must be on your project's own domain or a subdomain of it. For https://example.com, both https://example.com/metrics and https://api.example.com/metrics work.
  • Must be public: no login, API key or cookies. We don't send credentials.
  • Must not redirect. We refuse any 3xx response, so give us the final URL.
  • IP addresses, localhost and .local hosts are refused.

Response

  • HTTP status 200 (any 2xx works).
  • A JSON body, at most 1 MB.
  • Respond within 10 seconds.

Body format

  • A JSON array with 1 to 5 objects.
  • Each object has "label" (string, 1 to 32 characters), "value" (number), and optionally "public" (boolean).
  • "value" must be a JSON number, not a string: 123, not "123". No null, NaN or Infinity.
  • "public" must be a JSON boolean: true or false, not "true". Leave it out to keep the metric label private.
  • Labels are case-insensitive and stored in uppercase: "Signups" becomes "SIGNUPS". Two items with the same label are refused.
  • Send the current value only. Don't send timestamps or history; we record the time ourselves.

Limits

  • At most 5 metrics per response.
  • At most 5 different labels per project, ever. Once a project has used 5 labels, a response with a new label fails. Keep your labels stable.
  • Replacing or disconnecting your endpoint keeps its metric history. To reset the label limit, use Delete history next to your endpoint in My projects.

Showing or hiding metrics

  • "public": true shows the metric label, numbers and chart to everyone.
  • "public": false, or no "public" field, blurs the label for visitors. Only the owner sees the private label while signed in.
  • To hide the numbers too, turn on "Growth only" for that metric on your project page. Visitors then see the chart's shape and the % change, but not the values.
  • Your latest reading decides whether the label is visible for the whole series.
  • The public JSON API also masks private labels. For growth-only metrics it returns values rescaled so the largest is 100, never the real numbers.
  • Growth only works for Stripe MRR too: it stays Verified, but visitors see the % change instead of the dollar amount.

Public JSON API

Base URL: https://api.saasclimb.com

  • GET /leaderboard: every listed project with its series. Each series has its last reading per UTC day, up to 30 days.
  • GET /projects/{id}: one project with every reading. {id} is the 6-character id in the project's URL, e.g. /p/a1b2c3/acme.
  • GET /projects/{id}/metrics: just the series. Add ?label=DAU to get one series.
  • No key or sign-in needed, and CORS is open, so you can call it from a browser.

Each series has these fields:

  • label: the metric name, or "???" when the owner keeps it private.
  • public: false when the label is private.
  • verified: true for Stripe MRR, false for numbers from the owner's own endpoint.
  • valuesHidden: true when the owner turned on Growth only. The values are then rescaled so the largest is 100: the shape and the % change are real, the numbers aren't.
  • values: readings as {timestamp, value}, oldest first.

Example (GET /projects/{id}/metrics):

[
  { "label": "MRR", "public": true, "verified": true, "valuesHidden": true,
    "values": [{ "timestamp": "2026-10-01T00:00:00.000Z", "value": 72.5 },
               { "timestamp": "2026-10-18T00:00:00.000Z", "value": 100 }] },
  { "label": "???", "public": false, "verified": false, "valuesHidden": false,
    "values": [{ "timestamp": "2026-10-18T05:00:00.000Z", "value": 312 }] }
]

Polling

  • We fetch your endpoint once as soon as you connect it. If that fetch fails, the connection is refused and the error is shown, so you can fix it right away.
  • That first reading is saved immediately, so your project gets data on day one.
  • After that we fetch it every hour, at the top of the hour (UTC), and keep every reading.
  • Each reading is timestamped to the hour, e.g. 14:00 UTC.
  • Only approved projects are polled. Pending projects get their first reading on connect, then wait for approval.
  • If a fetch fails, no reading is saved for that hour, and the error is shown in My projects. The next hour tries again.
  • Requests come with the headers accept: application/json and user-agent: saasclimb-bot/1.0 (+https://www.saasclimb.com).

Ranking

  • The homepage lists approved projects alphabetically, with all their metrics together.
  • Charts show the last reading of each UTC day, for the last 30 recorded days. Missing days are not filled in.
  • We do not display calculated 30-day growth. You can report a growth metric through your own endpoint.
  • Numbers from your own endpoint are labeled "Self-reported". Only Stripe MRR is labeled "Verified".
  • A project can connect Stripe and an endpoint at the same time. Each chart says which one its numbers come from, and the 5-metric limit applies to the endpoint alone.

Tips

  • Pick metrics that make sense when sampled hourly, such as active users in the last 24 hours, total signups, or current paying customers.
  • Return the same set of labels every time. Dropping a label for an hour just leaves a gap; adding a new one uses up one of your 5.
  • Your numbers are public on your project page unless you turn on Growth only for a metric. Only return what you're happy to share.

Examples

Next.js (App Router): app/metrics/route.ts

export async function GET() {
  const dau = await countActiveUsersLast24h(); // your own query
  const signups = await countSignups();
  return Response.json([
    { label: "DAU", value: dau, public: true },
    { label: "Signups", value: signups }, // private label: numbers remain public
  ]);
}

Express

app.get("/metrics", async (req, res) => {
  res.json([{ label: "DAU", value: await countActiveUsersLast24h(), public: true }]);
});

Check it before connecting

curl -s -i https://example.com/metrics
# Expect: HTTP 200, no redirect, and a body like
# [{"label":"DAU","value":1234,"public":true}]

Errors

ErrorFix
Metrics endpoint must use httpsUse an https:// URL.
Metrics endpoint must be on <your domain>Host the endpoint on your project's domain or a subdomain.
Endpoint redirected; use its final URLCheck for trailing-slash or www redirects and use the final URL.
Endpoint returned HTTP <status>Make sure the URL is public and returns 2xx.
Endpoint did not return JSONReturn a JSON body, e.g. with Response.json(...).
Unexpected format …Return an array like [{"label":"DAU","value":123,"public":true}], with numbers as numbers and public as true/false.
A project can have at most 5 metricsGo back to the labels you used before, or use Delete history to start over.

Connecting

Submit a project, or connect an endpoint to an existing one from My projects. Choose "My own endpoint (self-reported)" and paste the URL.