Docs / Concepts / API Conventions

API Conventions

Environments, authentication, rate limits, pagination, error format, and retention — the cross-cutting details every integration needs.

Mental Model

Cross-cutting rules every endpoint inherits. API key prefix picks the environment. Pagination, errors, and rate limits all follow a single standard shape — learn it once, apply it everywhere.

Quick Take
Two environments (live / test) — fully isolated, selected by API key prefix
100 req/60s per tenant default — 429 with Retry-After when throttled
All errors use RFC 7807 Problem Details with stable type URIs
Pagination is cursor-based with a standard {data, pagination} envelope

API Conventions

Cross-cutting rules that apply across every QuotaStack endpoint. Read this once; everything else builds on it.

Environments

Every tenant has two fully isolated environments: live and test. Customers, balances, subscriptions, ledger entries, and webhooks are partitioned by (tenant_id, environment) — nothing crosses the boundary.

You switch environments by using a different API key. The key’s prefix determines which environment your request lands in:

PrefixEnvironment
qs_live_...Live (production data)
qs_test_...Test / sandbox

Both keys are issued when a tenant is created. The admin dashboard shows data for whichever key you authenticated with.

Authentication

All requests require an X-API-Key header:

curl https://api.quotastack.io/v1/billable-metrics \
  -H "X-API-Key: qs_live_..."

API keys support optional scopes (credits:read, credits:write, subscriptions:write, etc.) set at key creation. A key with no scopes has full access within its environment.

Rotation: Create a new key (POST /v1/admin/api-keys), deploy your application with the new key, then revoke the old one (POST /v1/admin/api-keys/{id}/revoke). No downtime.

Rate limits

Default: 100 requests per 60 seconds per tenant. Applied as a shared bucket across all endpoints — no per-endpoint sub-limits today.

Throttled responses return 429 Too Many Requests with a Retry-After header indicating the number of seconds to wait before retrying.

Contact support if your workload routinely exceeds this — limits are generous defaults, not hard ceilings.

Pagination

List endpoints (ledger history, topups, etc.) use cursor-based pagination with a standard envelope:

{
  "data": [ /* array of resources */ ],
  "pagination": {
    "has_more": true,
    "next_cursor": "MDE5ZDZhZGUtOTE4ZC03ZjQ0LTgzYWEtMDAw..."
  }
}
Query paramDefaultMaxNotes
cursorOpaque base64 string; pass the previous response’s next_cursor to fetch the next page.
limit20100Number of items per page. Minimum 1.

When has_more is false, next_cursor is null and you’ve reached the end.

Error format (RFC 7807)

Every 4xx and 5xx response uses the RFC 7807 Problem Details format with Content-Type: application/problem+json:

{
  "status": 402,
  "type": "https://api.quotastack.io/errors/insufficient-credits",
  "title": "Insufficient Credits",
  "detail": "Customer balance is 0 mc, requested 1000 mc"
}

The type URI is a stable identifier — your code should switch on type, not the human-readable title.

Validation errors (422) add a validation_errors array:

{
  "status": 422,
  "type": "https://api.quotastack.io/errors/validation-error",
  "title": "Validation Error",
  "detail": "One or more fields failed validation",
  "validation_errors": [
    { "field": "credits", "message": "must be positive", "code": "invalid" }
  ]
}

Common error types

type URI suffixWhen
/errors/bad-requestMalformed request (invalid JSON, bad params)
/errors/unauthorizedMissing or invalid API key
/errors/forbiddenAPI key lacks required scope
/errors/not-foundResource does not exist
/errors/conflictIdempotency key mismatch, state conflict, or insufficient balance on strict operations
/errors/insufficient-creditsBalance below required amount for the operation
/errors/rate-limitedOver the request-rate threshold
/errors/validation-errorOne or more fields failed validation (includes validation_errors)
/errors/internalServer-side failure (safe to retry)

Session-specific (/errors/session-expired, /errors/invalid-token, /errors/account-locked, etc.) appear for admin-dashboard flows, not tenant API traffic.

Finding your tenant ID

Tenant IDs appear in the admin dashboard (Settings → Tenant) and in every JWT login response. Most integrations never need it — the API key identifies your tenant implicitly. Webhook configuration and a few tenant-level config endpoints take the tenant ID in the path; reach for the dashboard in that case.

Ledger and audit retention

Ledger entries are retained indefinitely. There is no cold-storage tier today. If you approach ~10M ledger entries per tenant, reach out — we’ll work through a retention strategy before you hit practical limits.

Timestamps

All timestamps are ISO 8601 in UTC (2026-04-14T10:30:00Z). The server rejects timestamps in the future for fields like occurred_at on usage events.

Common Mistakes

The mistakes developers typically make with this concept — and what to do instead.

×
Don't hardcode the environment in your URLs
Why
The environment is picked by the API key, not the URL. Swapping qs_live_* for qs_test_* is the only switch your code should need.
×
Don't switch on title in error responses
Why
The title is a human-readable string and may change. Switch on the type URI — it's the stable identifier (e.g. /errors/insufficient-credits).
×
Don't poll list endpoints without pagination
Why
Without a cursor you'll always get the first page. Read pagination.next_cursor and pass it back as ?cursor=... until has_more is false.
🤖
Building with an AI agent?
Get this page as markdown: /docs/concepts/conventions.md · Full index: /llms.txt