API v0.1.0

API reference

Multi-tenant arbitrary-precision counter service. It’s a small REST surface — authenticate with an organization API key, then add, subtract, clear, and read counters. This reference is generated from the counters.dev OpenAPI contract, so it never drifts from the API. Prefer a client library? Jump to the SDKs.

Base URL
https://api.counters.dev/v1

Client libraries

Official SDKs wrap the API with a fluent client and automatic per-counter batching. Published SDKs are in beta and include verified install commands; unpublished guides show their status and point to the stable REST API.

TypeScriptBeta

@counters.dev/sdk

PythonUnpublished

PyPI package pending

GoUnpublished

Go modules package pending

JavaBeta

dev.counters:counters-sdk

C#Unpublished

NuGet package pending

PHPUnpublished

Packagist package pending

RubyUnpublished

RubyGems package pending

RustUnpublished

crates.io package pending

DartUnpublished

pub.dev package pending

KotlinUnpublished

Maven Central package pending

SwiftUnpublished

Swift Package Manager package pending

ZigUnpublished

a package registry package pending

Authentication

Organization API key. Verified locally; never round-trips to WorkOS on the request hot path. All data is scoped to the key’s organization. Keep the key server-side — put it in an environment variable or secret, never in client code.

Authorization header
curl "https://api.counters.dev/v1/counters" \
  -H "Authorization: Bearer $COUNTERS_API_KEY"
Create an API key

Conventions

Arbitrary precision

amount and value are decimal-digit strings, never JSON numbers — a JSON number is an IEEE-754 double and loses precision above 253. Counters never overflow.

Idempotent writes

Every write accepts an Idempotency-Key header. Retrying with the same key is de-duplicated within the dedup window — at-least-once delivery becomes effectively-once.

Clear vs. delete

clear starts a new epoch — the value resets to zero but history is retained. delete tombstones the counter. Counters may also go negative.

Batch is the fast path

/batch coalesces many operations into one call, each with its own idempotency key. It’s the primary write path for the SDKs — reach for it before looping single writes.

Errors

Errors use RFC 9457 problem details (application/problem+json) with a consistent shape:

FieldTypeDescription
typestring
titlestring
statusinteger
detailstring
instancestring
example error
{
  "type": "https://counters.dev/errors/quota-exceeded",
  "title": "Counter limit reached",
  "status": 403,
  "detail": "Your plan allows 100 counters; delete one or upgrade."
}
400Invalid request.401Missing or invalid API key.403A plan limit was reached (e.g. maximum counters).404Counter not found.429Rate limit exceeded.

counters

Counter registry and metadata.

GET/counters

List counters in the organization

Parameters

NameInTypeDescription
cursorquerystringOpaque pagination cursor from a previous response.
limitqueryinteger (1–200, default 50)

Request

curl
curl -X GET "https://api.counters.dev/v1/counters" \
  -H "Authorization: Bearer $COUNTERS_API_KEY"
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
  },
});
const data = await res.json();

Responses

StatusDescription
200A page of counters.(CounterPage)
401Missing or invalid API key.(Problem)
429Rate limit exceeded.(Problem)
200 · example response
{
  "data": [
    {
      "key": "registrations",
      "value": "0",
      "epoch": 3,
      "createdAt": "2026-01-01T00:00:00Z",
      "updatedAt": "2026-01-01T00:00:00Z"
    }
  ],
  "nextCursor": "eyJvZmZzZXQiOjUwfQ"
}
GET/counters/{counterKey}

Get a counter's metadata and current value

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").

Request

curl
curl -X GET "https://api.counters.dev/v1/counters/your-counter-key" \
  -H "Authorization: Bearer $COUNTERS_API_KEY"
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
  },
});
const data = await res.json();

Responses

StatusDescription
200The counter.(Counter)
401Missing or invalid API key.(Problem)
404Counter not found.(Problem)
200 · example response
{
  "key": "registrations",
  "value": "0",
  "epoch": 3,
  "createdAt": "2026-01-01T00:00:00Z",
  "updatedAt": "2026-01-01T00:00:00Z"
}

operations

Mutations — add, subtract, clear, delete, and batch.

DELETE/counters/{counterKey}

Delete (tombstone) a counter

Marks the counter deleted and stops serving it; events are purged asynchronously per retention.

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").
Idempotency-Keyheaderstring (len 0–255)Client-supplied key; retries with the same key are de-duplicated within the dedup window.

Request

curl
curl -X DELETE "https://api.counters.dev/v1/counters/your-counter-key" \
  -H "Authorization: Bearer $COUNTERS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
  },
});

Responses

StatusDescription
204Deleted.
401Missing or invalid API key.(Problem)
404Counter not found.(Problem)
POST/counters/{counterKey}/add

Add to a counter

Increments by a non-negative amount. Creates the counter if absent (subject to the plan's counter limit).

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").
Idempotency-Keyheaderstring (len 0–255)Client-supplied key; retries with the same key are de-duplicated within the dedup window.

Request body (required)

FieldTypeDescription
amount *stringNon-negative integer magnitude, arbitrary precision, as a decimal-digit string.pattern: ^[0-9]+$

Request

curl
curl -X POST "https://api.counters.dev/v1/counters/your-counter-key/add" \
  -H "Authorization: Bearer $COUNTERS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount":"1"}'
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key/add", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"amount":"1"}),
});
const data = await res.json();

Responses

StatusDescription
200Applied; returns the new value.(Counter)
400Invalid request.(Problem)
401Missing or invalid API key.(Problem)
403A plan limit was reached (e.g. maximum counters).(Problem)
429Rate limit exceeded.(Problem)
200 · example response
{
  "key": "registrations",
  "value": "0",
  "epoch": 3,
  "createdAt": "2026-01-01T00:00:00Z",
  "updatedAt": "2026-01-01T00:00:00Z"
}
POST/counters/{counterKey}/subtract

Subtract from a counter

Decrements by a non-negative amount. The counter may go negative.

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").
Idempotency-Keyheaderstring (len 0–255)Client-supplied key; retries with the same key are de-duplicated within the dedup window.

Request body (required)

FieldTypeDescription
amount *stringNon-negative integer magnitude, arbitrary precision, as a decimal-digit string.pattern: ^[0-9]+$

Request

curl
curl -X POST "https://api.counters.dev/v1/counters/your-counter-key/subtract" \
  -H "Authorization: Bearer $COUNTERS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount":"1"}'
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key/subtract", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"amount":"1"}),
});
const data = await res.json();

Responses

StatusDescription
200Applied; returns the new value.(Counter)
400Invalid request.(Problem)
401Missing or invalid API key.(Problem)
429Rate limit exceeded.(Problem)
200 · example response
{
  "key": "registrations",
  "value": "0",
  "epoch": 3,
  "createdAt": "2026-01-01T00:00:00Z",
  "updatedAt": "2026-01-01T00:00:00Z"
}
POST/counters/{counterKey}/clear

Clear a counter (reset to zero)

Starts a new epoch; the current value becomes zero. Historical series are retained.

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").
Idempotency-Keyheaderstring (len 0–255)Client-supplied key; retries with the same key are de-duplicated within the dedup window.

Request

curl
curl -X POST "https://api.counters.dev/v1/counters/your-counter-key/clear" \
  -H "Authorization: Bearer $COUNTERS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key/clear", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
  },
});
const data = await res.json();

Responses

StatusDescription
200Cleared; returns the counter at value 0 in the new epoch.(Counter)
401Missing or invalid API key.(Problem)
404Counter not found.(Problem)
200 · example response
{
  "key": "registrations",
  "value": "0",
  "epoch": 3,
  "createdAt": "2026-01-01T00:00:00Z",
  "updatedAt": "2026-01-01T00:00:00Z"
}
POST/batch

Apply a batch of operations

The SDK's primary write path. Operations are coalesced client-side and submitted together. Each operation carries its own idempotency key, so the batch is safe to retry. The HTTP call succeeding means the batch was accepted — inspect each per-operation result.

Request body (required)

FieldTypeDescription
operations *array<Operation>

Request

curl
curl -X POST "https://api.counters.dev/v1/batch" \
  -H "Authorization: Bearer $COUNTERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"operations":[{"counterKey":"registrations","op":"add","amount":"1","idempotencyKey":"b1a7c1e2-3f4d-5a6b-7c8d-9e0f1a2b3c4d"}]}'
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"operations":[{"counterKey":"registrations","op":"add","amount":"1","idempotencyKey":"b1a7c1e2-3f4d-5a6b-7c8d-9e0f1a2b3c4d"}]}),
});
const data = await res.json();

Responses

StatusDescription
200Per-operation results.(BatchResponse)
400Invalid request.(Problem)
401Missing or invalid API key.(Problem)
429Rate limit exceeded.(Problem)
200 · example response
{
  "results": [
    {
      "counterKey": "registrations",
      "status": "applied",
      "value": "0",
      "error": {
        "type": "string",
        "title": "string",
        "status": 0,
        "detail": "string",
        "instance": "string"
      }
    }
  ]
}

read

Reads — current value and time series.

GET/counters/{counterKey}/value

Get a counter's current value

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").

Request

curl
curl -X GET "https://api.counters.dev/v1/counters/your-counter-key/value" \
  -H "Authorization: Bearer $COUNTERS_API_KEY"
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key/value", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
  },
});
const data = await res.json();

Responses

StatusDescription
200The current value.(ValueResponse)
401Missing or invalid API key.(Problem)
404Counter not found.(Problem)
200 · example response
{
  "key": "registrations",
  "value": "0",
  "epoch": 3
}
GET/counters/{counterKey}/series

Get a counter's time series (delta per bucket)

Returns the per-bucket change in the counter over [from, to). Granularity (`bucket`) and lookback are constrained by the organization's plan. Empty buckets are omitted unless `gapfill=true`; clients treat a missing bucket as zero.

Parameters

NameInTypeDescription
counterKey *pathstring (len 1–200)Counter identifier, unique within the organization (e.g. "registrations").
from *querystring · date-time
to *querystring · date-time
bucket *queryenum: 1m | 5m | 1h | 1d | 1w | 1moBucket size. Allowed values depend on plan (finer buckets require higher plans).
modequeryenum: deltaOnly delta-per-bucket is supported in v0.1; cumulative is a future, separate read path.
tzquerystring (default "UTC")IANA timezone for calendar bucket boundaries (e.g. Europe/London).
gapfillqueryboolean (default false)

Request

curl
curl -X GET "https://api.counters.dev/v1/counters/your-counter-key/series?from=2026-01-01T00%3A00%3A00Z&to=2026-01-08T00%3A00%3A00Z&bucket=1m" \
  -H "Authorization: Bearer $COUNTERS_API_KEY"
TypeScript (fetch)
const res = await fetch("https://api.counters.dev/v1/counters/your-counter-key/series?from=2026-01-01T00%3A00%3A00Z&to=2026-01-08T00%3A00%3A00Z&bucket=1m", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.COUNTERS_API_KEY}`,
  },
});
const data = await res.json();

Responses

StatusDescription
200The series.(SeriesResponse)
400Invalid request.(Problem)
401Missing or invalid API key.(Problem)
403Requested granularity or lookback exceeds the plan's entitlement.(Problem)
200 · example response
{
  "counterKey": "registrations",
  "bucket": "string",
  "mode": "delta",
  "tz": "string",
  "range": {
    "from": "2026-01-01T00:00:00Z",
    "to": "2026-01-08T00:00:00Z"
  },
  "points": [
    {
      "t": "2026-01-01T00:00:00Z",
      "v": "0"
    }
  ]
}