Docs › Integrations
Integration guidePer-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) | Example | Limit |
|---|---|---|
X-MSA-Customer-Id | cus_42 | 64 chars; email-like IDs are hashed |
X-MSA-Plan | pro | 32 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