Start free

Docs › Integrations

Integration guide

Per-customer request tracking for Express, FastAPI and Hono

API Meter has no SDK. It's a reverse proxy: your clients call the proxy URL, the proxy forwards the request to your origin unchanged, and records one event per request. To attribute that event to a customer and plan, your origin adds two response headers. The proxy reads them and strips them before the response reaches the client.

Header (set on the response by your origin)ExampleLimit
X-MSA-Customer-Idcus_4264 chars; email-like IDs are hashed
X-MSA-Planpro32 chars, lowercased

If you don't set them, the proxy falls back to x-customer-id / x-customer-plan request headers, then to a salted hash of the x-api-key or Authorization: Bearer key, then anonymous. See the header reference.

Step 1: point clients at the proxy

After you sign up and verify your origin, swap the base URL. Everything after /p/<slug> is appended to your origin, query string included.

// JavaScript client
const BASE = "https://api-meter-proxy.marc-cf2.workers.dev/p/your-slug"; // was https://api.acme.dev
await fetch(`${BASE}/v1/orders/123`, { headers: { "x-api-key": key } });

# Python client
BASE = "https://api-meter-proxy.marc-cf2.workers.dev/p/your-slug"
requests.get(f"{BASE}/v1/orders/123", headers={"x-api-key": key})

Step 2a: Express (Node.js)

Add this after your auth middleware, wherever you know which customer owns the key. lookupKey stands for your own key lookup.

app.use(async (req, res, next) => {
  const acct = await lookupKey(req.get("x-api-key")); // your code
  if (acct) {
    res.set("X-MSA-Customer-Id", acct.customerId);
    res.set("X-MSA-Plan", acct.plan);
  }
  next();
});

A drop-in Express middleware that posts events without the proxy is on the roadmap and not released. The snippet above is plain Express and only works together with the proxy.

Step 2b: FastAPI (Python)

from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def tag_customer(request: Request, call_next):
    response = await call_next(request)
    acct = lookup_key(request.headers.get("x-api-key"))  # your code
    if acct:
        response.headers["X-MSA-Customer-Id"] = acct.customer_id
        response.headers["X-MSA-Plan"] = acct.plan
    return response

Step 2c: Hono on a Cloudflare Worker

If your API itself runs on Workers, tag responses the same way:

import { Hono } from "hono";
const app = new Hono();

app.use("*", async (c, next) => {
  await next();
  const acct = await lookupKey(c.req.header("x-api-key"), c.env); // your code
  if (acct) {
    c.header("X-MSA-Customer-Id", acct.customerId);
    c.header("X-MSA-Plan", acct.plan);
  }
});

app.get("/v1/orders", (c) => c.json({ orders: [] }));
export default app;

Your Worker's public URL is the origin you register at signup. Clients then call the API Meter proxy URL, not the Worker directly.

Step 3: check it works

# the demo origin sets X-MSA-Customer-Id: cus_alice, X-MSA-Plan: pro for this key
curl -i https://api-meter-proxy.marc-cf2.workers.dev/p/demo/v1/orders -H "x-api-key: demo_key_alice"

The response has an x-msa-request-id header and no X-MSA-Customer-Id / X-MSA-Plan: the proxy removed them. The request shows up under cus_alice / pro in the live demo dashboard.

Optional hardening: the proxy sends your origin an x-msa-proxy-secret header (the secret from signup). Reject requests without it if you want all traffic to go through the proxy. Beta: capture is capped at about 1,000 events/day across all APIs; traffic is always forwarded.

Try it

Start free, no SDK See the live demo

Related: Docs · Cloudflare API analytics without an SDK · Per-customer API analytics