# SaaSClimb self-reported metrics endpoint

How to expose your project's numbers to SaaSClimb (https://www.saasclimb.com) with your own JSON endpoint.

## 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:

```json
[
  { "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`):

```json
[
  { "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

```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

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

### Check it before connecting

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

## Errors

| Error | Fix |
| --- | --- |
| Metrics endpoint must use https | Use 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 URL | Check 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 JSON | Return 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 metrics | Go back to the labels you used before, or use Delete history to start over. |

## Connecting

Submit a project at https://www.saasclimb.com/submit, or connect an endpoint to an existing project from https://www.saasclimb.com/me. Choose "My own endpoint (self-reported)" and paste the URL.
