Code examples

Copy-paste snippets for the Usage Lab API. No SDK needed. Set USAGE_LAB_KEY to your API key.

curl

# Single event
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/usage \
  -H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: req_8f2c1" \
  -d '{"meter":"tokens","subscription":"cus_123","value":1500,"properties":{"model":"small"}}'

# Batch (up to 100). Events without their own key get "batch_77:0", "batch_77:1", ...
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/usage \
  -H "X-Api-Key: $USAGE_LAB_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: batch_77" \
  -d '[{"meter":"api_calls","subscription":"cus_123"},
       {"meter":"api_calls","subscription":"cus_456"},
       {"meter":"mau","subscription":"cus_123","unique_value":"user_42","idempotency_key":"mau_cus_123_user_42_2026-10"}]'

# Late event (up to 30 days back) lands in the month it happened
curl -X POST https://usage-lab.marc-cf2.workers.dev/api/v1/usage \
  -H "Authorization: Bearer $USAGE_LAB_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: job_991" \
  -d '{"meter":"tokens","subscription":"cus_123","value":800,"timestamp":"2026-09-29T23:10:00Z"}'

# Rollup for one subscription
curl "https://usage-lab.marc-cf2.workers.dev/api/v1/meters/tokens/rollups?month=2026-10&subscription=cus_123" \
  -H "Authorization: Bearer $USAGE_LAB_KEY"

Node (fetch, Node 18+)

const API = "https://usage-lab.marc-cf2.workers.dev";

export async function recordUsage(event, idempotencyKey) {
  const res = await fetch(`${API}/api/v1/usage`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.USAGE_LAB_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(event),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`);
  return body; // { event: { status, id, ... }, duplicate }
}

await recordUsage({ meter: "tokens", subscription: "cus_123", value: 1500 }, "req_8f2c1");

const r = await fetch(`${API}/api/v1/meters/tokens/rollups?month=2026-10`, {
  headers: { "Authorization": `Bearer ${process.env.USAGE_LAB_KEY}` },
});
console.log((await r.json()).rollups);

Python (requests)

import os, requests

API = "https://usage-lab.marc-cf2.workers.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['USAGE_LAB_KEY']}"}

def record_usage(event: dict, idempotency_key: str) -> dict:
    r = requests.post(f"{API}/api/v1/usage", json=event,
                      headers={**HEADERS, "Idempotency-Key": idempotency_key}, timeout=10)
    body = r.json()
    if not r.ok:
        raise RuntimeError(f"{r.status_code} {body['error']['code']}: {body['error']['message']}")
    return body

record_usage({"meter": "tokens", "subscription": "cus_123", "value": 1500}, "req_8f2c1")

rollups = requests.get(f"{API}/api/v1/meters/tokens/rollups",
                       params={"month": "2026-10"}, headers=HEADERS, timeout=10).json()
print(rollups["total_quantity"], rollups["rollups"])

Retry-safe pattern

Pick the idempotency key once, from something stable in your system (request id, job id, message id), and reuse it on every retry. Retry network errors, 5xx and 429; don't retry other 4xx. A 409 means you reused a key for a different event, which is a bug in your keys, not a transient error. See deduping usage events with idempotency keys.

// Node: exponential backoff, same key every attempt
export async function recordUsageWithRetry(event, key, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch("https://usage-lab.marc-cf2.workers.dev/api/v1/usage", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.USAGE_LAB_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": key,                // never regenerate on retry
        },
        // send an explicit timestamp so a retry hours later still lands in the right month
        body: JSON.stringify({ ...event, timestamp: event.timestamp ?? new Date().toISOString() }),
      });
      if (res.ok) return res.json();             // 201 created or 200 duplicate: both mean "counted once"
      if (res.status !== 429 && res.status < 500) {
        throw Object.assign(new Error(`${res.status}`), { body: await res.json(), fatal: true });
      }
    } catch (err) {
      if (err.fatal) throw err;                   // 400/401/404/409/422: fix, don't retry
    }
    await new Promise(r => setTimeout(r, 250 * 2 ** i + Math.random() * 100));
  }
  throw new Error("usage not recorded after retries; queue it and replay with the same key");
}

Set the timestamp once, before the first attempt, and keep it in the retry: the timestamp is part of the compared payload when you send one.

Cloudflare Worker middleware

Meter every request to your API without touching handlers. ctx.waitUntil sends the event after the response is returned, so it adds no latency. Store the key with wrangler secret put USAGE_LAB_KEY. This example assumes a count meter called api_calls and that your auth puts the customer id in a header.

export default {
  async fetch(request, env, ctx) {
    const response = await handle(request, env);           // your existing app

    const customer = request.headers.get("x-customer-id");  // however you identify the payer
    if (customer && response.ok) {
      const key = request.headers.get("cf-ray") ?? crypto.randomUUID(); // unique per request
      ctx.waitUntil(
        fetch("https://usage-lab.marc-cf2.workers.dev/api/v1/usage", {
          method: "POST",
          headers: {
            "Authorization": `Bearer ${env.USAGE_LAB_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": key,
          },
          body: JSON.stringify({
            meter: "api_calls",
            subscription: customer,
            timestamp: new Date().toISOString(),
            properties: { path: new URL(request.url).pathname },
          }),
        }).catch(err => console.error("usage-lab", err))
      );
    }
    return response;
  },
};

async function handle(request, env) {
  return new Response("hello");
}

waitUntil is best effort: if the call fails it is not retried. For revenue-critical metering, push events to a Cloudflare Queue and send them from the consumer with the same idempotency key.