DocsRecipes

Recipes

API usage customer portal

3 min readReviewed July 2026

This architecture gives each customer a dashboard for request volume, error rate, and latency without exposing data from another tenant. It is suitable for product visibility; keep authoritative metering and invoices in a transactional billing system.

Capture one completion event#

Emit one event after the API response status and duration are known:

{
  "event": "api_request_completed",
  "event_id": "req_01J...",
  "account_id": "acct_7",
  "route": "/v1/reports/:id",
  "method": "GET",
  "status_code": 200,
  "status_family": "2xx",
  "duration_ms": 184,
  "request_bytes": 420,
  "response_bytes": 18240,
  "region": "us-west",
  "api_version": "2026-07-01",
  "schema_version": 1
}

Use a normalized route template, not the raw path /v1/reports/rpt_391. Raw paths create unbounded dimensions and can contain sensitive identifiers or query strings.

Do not include authorization headers, tokens, request bodies, response bodies, or arbitrary headers. If a support workflow needs request-level lookup, keep an opaque event_id that can be resolved in trusted operational logs.

Place delivery off the hot path#

The API response should not wait on an analytics network call. Publish the compact event to a local outbox or durable queue after the response fields are known. A batch worker sends up to 50 events at a time to an api_events collection.

Monitor queue age and dead letters. If events support customer-visible usage, reconcile them against gateway counters. If they support billing, the billing ledger—not GraphJSON—must remain authoritative.

At high request volume, coordinate expected peaks and payload size with GraphJSON before launch. Sampling can be appropriate for aggregate latency research but not for per-customer totals.

Define customer metrics#

Request volume:

SELECT
  toUnixTimestamp(
    toStartOfDay(toDateTime(timestamp, 'UTC'))
  ) AS day,
  count() AS requests
FROM api_events
WHERE
  JSONExtractString(json, 'account_id') = 'acct_7'
  AND toDateTime(timestamp) >= subtractDays(now(), 30)
GROUP BY day
ORDER BY day

Error rate:

SELECT
  toUnixTimestamp(
    toStartOfDay(toDateTime(timestamp, 'UTC'))
  ) AS day,
  round(
    100 * countIf(
      JSONExtractInt(json, 'status_code') >= 500
    ) / nullIf(count(), 0),
    2
  ) AS server_error_rate_pct
FROM api_events
WHERE
  JSONExtractString(json, 'account_id') = 'acct_7'
  AND toDateTime(timestamp) >= subtractDays(now(), 30)
GROUP BY day
ORDER BY day

Latency:

SELECT
  JSONExtractString(json, 'route') AS route,
  quantileExact(0.50)(
    JSONExtractFloat(json, 'duration_ms')
  ) AS p50_ms,
  quantileExact(0.95)(
    JSONExtractFloat(json, 'duration_ms')
  ) AS p95_ms,
  count() AS requests
FROM api_events
WHERE
  JSONExtractString(json, 'account_id') = 'acct_7'
  AND toDateTime(timestamp) >= subtractDays(now(), 7)
GROUP BY route
ORDER BY requests DESC
LIMIT 25

Choose whether 4xx responses represent customer errors, authentication failures, or expected validation. Present 4xx and 5xx separately rather than combining unlike failure modes into one “error rate.”

Generate tenant-safe reports#

Do not hard-code acct_7 in a customer-facing query. Build the report in GraphJSON with a test filter, export its API configuration, and have your server replace only the account filter with the ID from an authorized session.

Use one server-owned report registry:

  • api_requests_30d
  • api_error_rate_30d
  • api_latency_by_route_7d
  • api_volume_by_status_24h

The browser selects a report name, not arbitrary SQL or filters. Cache generated iframe URLs by report name, account ID, timezone, and configuration version.

Design the portal experience#

Start with:

  1. request total and change from prior period
  2. daily request trend
  3. 4xx and 5xx rates
  4. p50 and p95 latency
  5. top normalized routes

Display the reporting time zone and range. Reserve chart space while loading, distinguish empty data from failure, and provide an explanation for status families and latency percentiles.

For plan limits, show the billing system’s authoritative usage alongside the analytics view or clearly label any timing difference. Do not let a late analytics event determine whether a customer request is accepted.

Operational alerts#

Create internal alerts for:

  • abrupt total-volume drops
  • increased 5xx rate
  • p95 latency regression
  • delivery queue age
  • missing account_id

A total drop can indicate broken instrumentation rather than healthy traffic. Compare service health and ingestion health before escalating to customers.

Launch checklist#

  • Route values are normalized and contain no query strings.
  • Headers and bodies are excluded or strictly allowlisted.
  • Event delivery is off the request latency path.
  • Billing uses a separate authoritative ledger.
  • 4xx, 5xx, latency, and volume have written definitions.
  • Every report receives a server-authorized account_id.
  • Cache and browser tests prove cross-tenant isolation.
  • Portal metrics reconcile against gateway counters.
Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support