Skip to content

08 · API Monetization & Developer Portals

Some APIs are the product itself — customers pay directly for access. This module covers metering usage, billing for it, and the developer portal experience that makes a paid API self-service.

Metering: counting what customers use

curl https://api.example.com/v1/geocode?address=1600+Amphitheatre+Pkwy \
  -H "Authorization: Bearer sk_live_..."
HTTP/1.1 200 OK
X-Usage-Billed: 1

Every billable call increments a per-account usage counter, typically recorded asynchronously (so metering never adds latency to the actual response) via the same event-driven pattern from module 7:

{ "event": "api_call.billed", "account_id": "acct_42", "endpoint": "/v1/geocode", "units": 1 }

Pricing models

Tier        | Included calls/mo | Overage       | Price
------------|--------------------|--------------|----------
Free         | 1,000             | blocked       | $0
Pro          | 100,000           | $0.001/call  | $49/mo
Enterprise   | Custom            | Custom        | Custom (negotiated)
curl https://api.example.com/v1/geocode -H "Authorization: Bearer $FREE_TIER_KEY"
{ "error": { "code": "quota_exceeded", "message": "Free tier limit of 1,000 calls/mo reached. Upgrade at https://example.com/pricing" } }

HTTP/1.1 429 Too Many Requests

Unlike the rate limiting in Level 2 (fairness, resets every window), quota exhaustion here is a billing boundary — it resets on the billing cycle, not every minute, and the fix for the customer is upgrading a plan, not waiting 60 seconds.

Usage dashboards: self-service visibility

curl https://api.example.com/v1/account/usage \
  -H "Authorization: Bearer $DASHBOARD_TOKEN"
{
  "period": "2026-08",
  "calls_used": 84213,
  "calls_included": 100000,
  "projected_overage_cost_usd": 0.00,
  "breakdown": { "/v1/geocode": 71000, "/v1/reverse-geocode": 13213 }
}

Giving developers this data directly, in real time, avoids the worst outcome for a paid API: a customer discovering a huge bill at the end of the month with no way to have seen it coming.

The developer portal as the whole self-service loop

A monetized API's portal typically bundles:

  • Sign-up → key generation (module 4)
  • Interactive docs and quickstart (Level 3, module 7)
  • Live usage dashboard (above)
  • Billing/invoices and plan upgrade, self-service, no sales call needed for the lower tiers
  • Support ticket / community forum link
curl -X POST https://api.example.com/v1/account/plan \
  -H "Authorization: Bearer $DASHBOARD_TOKEN" \
  -d '{"plan": "pro"}'
{ "plan": "pro", "effective": "2026-09-01", "next_invoice_usd": 49.00 }

Worked example: designing quota enforcement without hurting reliability

A naive implementation checks quota synchronously against a database on every single call — adding latency and a new point of failure to every request, and risking overselling if two concurrent requests both read "999 of 1000 used" before either writes back 1000.

A better design:

  1. Maintain a fast, in-memory (Redis) counter per account, incremented atomically (INCR) on every call — no read-then-write race.
  2. Check the counter against the plan's limit synchronously (this part must be fast and correct) — return 429 quota_exceeded immediately if over.
  3. Asynchronously reconcile the fast counter against the durable billing ledger (via the event stream from module 7) for accurate invoicing, decoupled from the hot request path.
  4. Reset the counter on the billing cycle boundary, not a rolling window.

How It Actually Works

Metering usage for billing means every request has to be counted and attributed to a specific customer reliably, even under failures and retries — the mechanism is closer to financial ledger-keeping than simple logging.

A typical metering pipeline:

1. Request arrives with API key -> resolved to customer_id (module 4).
2. Handler completes -> emit a usage event:
   { customer_id, endpoint, timestamp, request_id }
3. Event goes to a durable queue (not counted in-process synchronously,
   because an in-memory counter is lost on a crash/restart).
4. A separate aggregation job sums events per customer per billing period.

The request_id in that event is what makes billing idempotent against duplicate counting: if step 3's queue delivers the same event twice (the at-least-once guarantee from event-driven systems, module 7), the aggregation job deduplicates by request_id before summing — without this, a network retry or a broker redelivery directly inflates a customer's bill, a bug class with real financial consequences, not just a display glitch.

Tiered rate limits per plan reuse the token-bucket mechanism (module 6, Level 2) but with capacity/refill_rate looked up per customer's plan rather than one global constant — the same rate-limiting code path runs for every customer, just parameterized differently, which is why billing-plan changes (a customer upgrading tiers) can take effect immediately: it's a config lookup change, not a code deploy, as long as the plan-to-limits mapping is read fresh (or from a short-TTL cache) on each request rather than baked in at startup.

Exercise

  1. Why is a naive read-then-write quota check subject to a race condition under concurrent requests, and how does an atomic increment (INCR) fix it?
  2. Explain the difference between rate limiting (Level 2, module 9) and billing quota enforcement — why do they need different reset windows and different client-facing remedies?
  3. Design the response body and status code for a customer who's 80% through their monthly quota — should this ever be surfaced proactively, and how?
  4. A customer disputes an invoice, claiming the usage count is wrong. What data would you need to have been logging all along to resolve this dispute with confidence?