Documentation

counr

counr is an API that decides whether something can happen — based on usage and limits.

No Redis scripts to write or maintain, no counters, no edge cases — just a single decision API.

Use it for rate limiting, quotas, credits, and feature gating.

All requests require an API key.

Authenticated, non-idempotent requests count toward account caps, including denied ones.

Quickstart

Make your first allow/deny decision in under 30 seconds.

1. Get your API key

Create an API key from the dashboard. A valid API key is required on every request.

2. Make your first request

Base URL: https://api.countr.dev

Replace ck_use_live_a1b2c3d4e5f67890abcd1234ef567890 with your API key.

curl "https://api.countr.dev/v1/check-consume" \
  -X POST \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "user_123",
    "metric": "api_calls",
    "cost": 1
  }'

Or use the SDK:

npm install countr-sdk

Use the SDK once you're up and running for easier integration.

Response — allowed

{
  "allowed":   true,
  "remaining": 997,
  "reason":    null
}

Response — denied (account cap)

{
  "allowed":   false,
  "remaining": 0,
  "reason":    "owner_rate_limit_exceeded"
}

Response — denied (subject limit)

{
  "allowed":   false,
  "remaining": 3,
  "reason":    "limit_exceeded"
}

Requests are denied when remaining < cost, so blocked responses may still show a positive remaining when cost > 1.

Core Concepts

owner — you, the API key holder. Your account-level limits apply to all requests you make.

subject — the entity being tracked (a user ID, IP address, tenant, etc.). Each subject has its own usage counter.

metric — what you're counting (api_calls, ai_tokens, exports). Configure metrics and limits in the dashboard.

cost — how much to consume. Usually 1 for simple rate limiting, or a token count for AI usage.

What You Can Build

Rate limiting

Block requests after a threshold. Set a daily limit on api_calls and check on every request.

Quotas

Limit usage per day or month. Counters reset automatically — no cron jobs needed.

Credits

Charge per action. Pass the actual cost (e.g. token count) as cost.

Feature gating

Restrict features by plan. Use subject overrides to give Pro users a higher (or unlimited) limit.

Why counr

Instead of building this with Redis + Lua scripts:

  • No race conditions to solve
  • No distributed state to manage
  • No infra to provision or maintain
  • Multiple limits handled in one call
  • Works globally with a single API

Failure Behaviour

Default: fail open (recommended for most apps)

counr is designed for the request path, but failures can happen. If counr cannot be reached, you decide whether to allow or deny the request based on your risk tolerance.

Fail open (recommended for most apps)

Allow the request if counr is unreachable. Keeps your app available during outages.

import { CountrClient } from "countr-sdk";

const client = new CountrClient({ apiKey: "ck_use_live_a1b2c3d4e5f67890abcd1234ef567890", baseUrl: "https://api.countr.dev" });

function shouldFailOpen(error) {
  const status = error?.status ?? error?.statusCode;
  if (typeof status === "number") return status >= 500;
  const code = typeof error?.code === "string" ? error.code : "";
  const name = typeof error?.name === "string" ? error.name : "";
  return (
    ["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "ETIMEDOUT", "ECONNABORTED"].includes(code)
    || name === "AbortError"
    || name === "TimeoutError"
  );
}

async function isAllowed(subject, metric) {
  try {
    const result = await client.checkConsume({ subject, metric, cost: 1 });
    return result.allowed;
  } catch (error) {
    if (shouldFailOpen(error)) {
      return true; // fail open — allow only when Countr is unreachable
    }
    throw error;
  }
}

Fail closed

Block the request if counr is unreachable. Use only when strict enforcement is required.

import { CountrClient } from "countr-sdk";

const client = new CountrClient({ apiKey: "ck_use_live_a1b2c3d4e5f67890abcd1234ef567890", baseUrl: "https://api.countr.dev" });

async function isAllowed(subject, metric) {
  try {
    const result = await client.checkConsume({ subject, metric, cost: 1 });
    return result.allowed;
  } catch {
    return false; // fail closed — block if Countr is unreachable
  }
}

Performance

counr is designed for low-latency decisions and is safe to call on every request.

  • ~50ms average response time
  • <100ms p95 (US region)

For live status, see the status page.

Decisions

Every checkConsume call returns a decision:

  • allowed: true — proceed; subject usage is incremented only when a limit is configured (that is, when remaining is a number). If remaining is null, no limit is configured and the subject counter is not incremented
  • allowed: false — blocked; subject usage is not incremented, but the request still counts toward your account-wide caps (monthly/minute/second)
  • reason — set when allowed is false
reasonMeaning
limit_exceededSubject's limit was reached; wait for the window to reset if applicable, or increase the limit
owner_rate_limit_exceeded_secondToo many requests in a short burst on your account; try again in a moment
owner_rate_limit_exceededToo many requests per minute on your account
owner_monthly_limit_exceededAccount monthly cap reached; resets next calendar month

API Reference

POST/v1/check-consume

Checks whether a subject is within their limit for a given metric, then records the usage. Returns allowed: true and a remaining quota value when the request is permitted (where remaining is a number, or null when no limit is configured / quota is unlimited), or allowed: false with a reason field when the request is denied. Requests may be denied by a subject/metric limit (reason: "limit_exceeded"), the account-wide monthly cap (reason: "owner_monthly_limit_exceeded"), the account-wide per-minute rate limit (reason: "owner_rate_limit_exceeded"), or the per-second burst limit (reason: "owner_rate_limit_exceeded_second"). All accounts are subject to account-wide limits — requests may be rejected if the account-level monthly, per-minute, or burst limit is exceeded regardless of any subject-specific limits configured.

Monthly allowance: authenticated, non-idempotent counr API requests count toward your monthly allowance, whether they are allowed or rejected due to account-level limits. Subject-level limit denials do not consume subject quota, but they do count as API requests toward your account cap. Idempotency replays do not count again toward owner/account caps or counters.

Authorization

Authorization: Bearer <token>

Pass your API key as a Bearer token.

Idempotency (optional)

Idempotency-Key: <string>

Use this to prevent double consumption when retrying on network timeouts or transient failures — for example, to avoid charging a user twice if your request to counr times out before you receive a response. An opaque string (max 100 chars): when the decision is allowed, the result is cached for 24 hours for both configured limits and unlimited subjects (including responses with remaining: null). Retried requests with the same key return the cached response without re-incrementing usage, and replays also bypass owner caps/counters. Denied decisions are not cached either; those always re-evaluate live state.

curl "https://api.countr.dev/v1/check-consume" \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Idempotency-Key: req_01J9ABCDEF" \
  -H "Content-Type: application/json" \
  -d '{"subject":"user_123","metric":"api_calls","cost":1}'

Request body

{
  "subject": "user_123",   // string, required, max 200 chars
  "metric":  "api_calls",  // string, required, max 200 chars
  "cost":    1             // positive integer, required
}

Example request

curl "https://api.countr.dev/v1/check-consume" \
  -X POST \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "user_123",
    "metric": "api_calls",
    "cost": 1
  }'

Success response · 200 · allowed

{
  "allowed":   true,
  "remaining": 997,
  "reason":    null
}

Success response · 200 · denied

{
  "allowed":   false,
  "remaining": 0,
  "reason":    "limit_exceeded"
}

Error response · 400 / 401 / 503

{
  "error": {
    "code":    "validation_error",
    "message": "Invalid request body",
    "details": {
      "cost": "Cost must be a positive integer"
    }
  }
}

Possible error.code values: validation_error (400 — invalid or missing fields), invalid_json (400 — malformed JSON body), unauthorized (401 — missing or invalid API key), service_unavailable (503 — service temporarily unavailable).

GET/v1/usage

Returns the current usage state for a subject and metric — without consuming any quota. Use this to display live usage to your end users or to check headroom before performing an action. This endpoint does not increment subject usage. Owner-level request caps still apply: each call counts against your account's monthly, per-minute, and per-second limits.

Authorization

Authorization: Bearer <token>

Pass your API key as a Bearer token.

Query parameters

subject  string  required  max 200 chars
metric   string  required  max 200 chars

Example request

curl "https://api.countr.dev/v1/usage?subject=user_123&metric=api_calls" \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890"

Success response · 200 · limit configured

{
  "subject":   "user_123",
  "metric":    "api_calls",
  "current":   58,
  "limit":     100,
  "remaining": 42,
  "window":    "day"
}

window reflects the configured time window: "none" (lifetime), "day" (resets at UTC midnight), or "month" (resets at the start of each UTC month). remaining is never negative.

Success response · 200 · no limit configured (unlimited)

{
  "subject":   "user_123",
  "metric":    "api_calls",
  "current":   0,
  "limit":     null,
  "remaining": null,
  "window":    "none"
}

When no limit is configured for the subject+metric, limit and remaining are null (unlimited). current will typically be 0 — usage counters are only incremented for subjects with a configured limit. A non-zero value may appear only if the subject previously had a lifetime limit ("window": "none") that was later removed.

Error response · 400 / 401 / 429 / 503

{
  "error": {
    "code":    "validation_error",
    "message": "Invalid query parameters",
    "details": {
      "subject": "Subject is required"
    }
  }
}

Possible error.code values: validation_error (400 — missing or invalid query params), unauthorized (401 — missing, invalid, or inactive API key), owner_monthly_limit_exceeded, owner_rate_limit_exceeded, owner_rate_limit_exceeded_second (429 — account-wide cap exceeded), service_unavailable (503 — service temporarily unavailable).

GET/v1/health

Checks that the API and Redis are reachable. No authentication required. Suitable for uptime monitoring (api.countr.dev).

Success response · 200

{ "status": "ok" }

Error response · 500

{ "status": "error" }
GET/health

Confirms the app is available. No authentication required. Suitable for uptime monitoring (countr.dev).

Success response · 200

{ "status": "ok" }

Account limits & plans

Every account is on a plan that controls monthly request caps, rate limits, and the number of API keys allowed. All limits apply across all subjects and metrics and count authenticated, non-idempotent request volume — idempotency-key replays served from cache are not counted.

PlanMonthly capPer-minutePer-secondAPI keys
Free2,0003051
Starter100,000120201
Growth500,000300503
Scale1,000,000 (+ overages)1,000100Unlimited

Overages available at ~$0.10 per 10,000 requests beyond included monthly allowance. Plan changes are handled manually at this time — contact us to upgrade.

When a request is denied due to an account-wide or subject-specific limit, the API returns a 200 response with allowed: false and one of the limit-related reasons below. Requests with an invalid or inactive API key receive a 401 error response instead.

reasonExplanation
owner_monthly_limit_exceededYou reached your monthly request limit for your plan
owner_rate_limit_exceededYou exceeded the per-minute request limit for your plan
owner_rate_limit_exceeded_secondToo many requests in a short burst
limit_exceededThe configured limit for this subject was exceeded

Authentication error responses

Authentication failures are returned as HTTP 401 JSON errors, not as { allowed: false, reason: ... } responses.

statuserror.codeExplanation
401unauthorizedAuthentication failed. Check error.message for the specific reason (invalid key, inactive key, missing or malformed Authorization header).

All authenticated, non-idempotent requests count toward your monthly allowance, whether allowed or denied.

Configure metrics & limits

Define the metrics you want to track and set limits in your dashboard. You can create any number of custom metrics, set owner-wide default limits, and override limits for specific subjects.

Limits support three time windows: Lifetime (usage accumulates indefinitely — the default), Daily (resets each UTC day), and Monthly (resets at the start of each UTC month). Once the window period ends, usage automatically starts fresh — no manual reset required.

Example limits

api_calls     1,000 / day      — resets each UTC midnight
ai_tokens    10,000 / month   — resets each calendar month
storage_bytes unbounded (lifetime)

Example: check and consume ai_tokens for a subject

curl "https://api.countr.dev/v1/check-consume" \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Content-Type: application/json" \
  -d '{"subject":"user_123","metric":"ai_tokens","cost":500}'

Dashboard

The dashboard is where you configure everything:

  • API Keys — create and revoke keys (formatted as ck_use_live_xxx)
  • Metrics — define the metrics you want to track (e.g. api_calls, ai_tokens); dashboard metric keys must be lowercase snake_case (max 64 chars) — if no limit is configured for a metric, requests against it are always allowed
  • Default Limits — set owner-wide limits per metric and time window
  • Subject Overrides — give a specific subject a higher or lower limit
  • Usage — view account usage for the current month and recent request decisions. The monthly summary reflects live account usage when available. The activity table shows recent requests and decisions for debugging (best-effort — may not include every request).

What counr stores

counr stores usage counters per subject and metric, and logs recent request decisions to power the dashboard usage view. Counters are scoped to your account and are used only for limit enforcement and visibility. counr does not store your application payloads; for request accounting and recent decision logs, it stores the subject identifier, metric key, cost, and decision metadata needed for enforcement and the dashboard. If you send an Idempotency-Key, that value is also persisted as part of Redis idempotency state for about 24 hours to deduplicate retries, so do not put sensitive data in it.

Examples

API rate limiting

Allow each user 1,000 API calls per day. Set a daily limit of 1000 on the api_calls metric, then call check-consume on each request:

curl "https://api.countr.dev/v1/check-consume" \
  -X POST \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "user_123",
    "metric": "api_calls",
    "cost": 1
  }'

AI token usage

Allow each user 100,000 tokens per month. Pass the actual token count as the cost:

curl "https://api.countr.dev/v1/check-consume" \
  -X POST \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "user_123",
    "metric": "ai_tokens",
    "cost": 1452
  }'

Per-user quotas with overrides

Set a default limit of 500 exports per month, then give a Pro user a higher limit via a subject override in the dashboard:

curl "https://api.countr.dev/v1/check-consume" \
  -X POST \
  -H "Authorization: Bearer ck_use_live_a1b2c3d4e5f67890abcd1234ef567890" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "pro_user_456",
    "metric": "exports",
    "cost": 1
  }'