Meters count what customers use (API calls, seats, gigabytes) and bill it each period. Make meters on the Meters tab of Subscriptions in the dashboard; this API sends usage to them and reads it back. How usage turns into invoices is on Usage billing; a complete sender is in the usage recipe. Reading needs subscriptions:read, sending subscriptions:write.
| Topic | Detail |
|---|---|
| Endpoints | `POST /api/v1/meter-events`, `POST /api/v1/meter-events/batch`, `GET /api/v1/meters`, `GET /api/v1/meters/{id}/usage` |
| Key permissions | subscriptions:write, subscriptions:read |
| Samples | curl; Node.js 18+ with the built-in fetch (in an ES module); the Node SDK (preview: not on npm yet); Python with requests. |
| Every request can also answer | 401 UNAUTHORIZED, 401 API_KEY_EXPIRED, 403 NOT_ENROLLED, 403 PERMISSION_DENIED, 403 IP_NOT_ALLOWED, 429 RATE_LIMITED, 500 INTERNAL_ERROR. See Error codes. |
The meter object
{
"object": "meter",
"livemode": true,
"id": "mtr_8fKq2LmNpR4sT6vW9xYz",
"name": "API calls",
"eventName": "api_call",
"aggregation": "count",
"unitLabel": "API call",
"unitPriceCents": "0.25",
"includedUnits": "1000",
"status": "active",
"createdAt": "2026-10-01T15:04:05.000Z"
}| Field | Type | Description |
|---|---|---|
object | string | The type of object. Always meter. |
livemode | boolean | False for test-mode data (an ak_test_ key); true for live data. |
id | string | mtr_ followed by 20 letters and digits. |
name | string | Shown on invoices. |
eventName | string | What your events name to reach this meter. Unique per business. |
aggregation | string | How a period's events add up: sum adds values, count counts events, max takes the highest value, last the latest. |
unitLabel | string | The unit, e.g. "API call". |
unitPriceCents | string | Price per unit in cents, as a decimal string (fractions of a cent allowed). |
includedUnits | string | Units each period before billing starts, as a decimal string. |
status | string | active, or archived (it stops taking events and billing). |
createdAt | string (date-time) | Created. |
The meter event object
{
"object": "meter_event",
"livemode": true,
"id": "mev_48213",
"meter": "mtr_8fKq2LmNpR4sT6vW9xYz",
"eventName": "api_call",
"customer": "cus_1042",
"customerEmail": "jordan@example.com",
"value": "1",
"timestamp": "2026-10-24T09:30:00.000Z",
"idempotencyKey": "req_7f3a9c",
"created": "2026-10-24T09:30:01.214Z"
}| Field | Type | Description |
|---|---|---|
object | string | The type of object. Always meter_event. |
livemode | boolean | False for test-mode data (an ak_test_ key); true for live data. |
id | string | mev_… |
meter | string | mtr_… |
eventName | string | The meter's event name. |
customer | string or null | cus_… when the customer is saved. |
customerEmail | string | Lower-cased. |
value | string | The usage, as a decimal string. |
timestamp | string (date-time) | When the usage happened. |
idempotencyKey | string | Your key for this event. |
created | string (date-time) | Recorded. |
The usage object
{
"object": "meter_usage",
"livemode": true,
"meter": "mtr_8fKq2LmNpR4sT6vW9xYz",
"customer": "cus_1042",
"customerEmail": "jordan@example.com",
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-10-03T00:00:00.000Z",
"aggregation": "count",
"quantity": "1210",
"eventCount": 1210,
"daily": [
{
"date": "2026-10-01",
"quantity": "480"
},
{
"date": "2026-10-02",
"quantity": "730"
}
]
}| Field | Type | Description |
|---|---|---|
object | string | The type of object. Always meter_usage. |
livemode | boolean | False for test-mode data (an ak_test_ key); true for live data. |
meter | string | mtr_… |
customer | string or null | cus_… when the customer is saved. |
customerEmail | string | Lower-cased. |
from | string (date-time) | Window start. |
to | string (date-time) | Window end. |
aggregation | string | One of sum, count, max, last. |
quantity | string | Usage over the window, as a decimal string. |
eventCount | integer | Events in the window. |
daily | object[] | |
daily[].date | string | The UTC day, YYYY-MM-DD. |
daily[].quantity | string | Usage that day, as a decimal string. |
Endpoints
Record usage
/api/v1/meter-eventssubscriptions:writeIdempotent by each event's idempotencyKey.Record one usage event against a meter, by the meter's event name. Each event carries its own idempotencyKey in the body (the Idempotency-Key header isn't used here): repeating it with the same event answers 200 with Idempotent-Replayed: true; with a different event, 409 IDEMPOTENCY_KEY_REUSED. 422 for a timestamp more than 5 minutes ahead, older than 400 days, or in a period already billed.
Body
eventNamestringrequired- The meter's event name.
customerIdstringcus_…. Send this orcustomerEmail.customerEmailstring- The customer's email. Send this or
customerId. valuenumber or string- The usage, at least 0 with up to 6 decimals. Defaults to 1.
timestampstring (date-time)- ISO 8601 with an offset. Defaults to now; at most 5 minutes ahead.
idempotencyKeystringrequired- Required. Your unique key for this event, 1–255 printable characters, such as your own request id. Sending it again with the same event answers
200with the event already recorded; nothing is counted twice.
curl -X POST https://anytime-capital.com/api/v1/meter-events \
-H "Authorization: Bearer $ANYTIME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventName": "api_call",
"customerId": "cus_1042",
"value": 1,
"idempotencyKey": "req_7f3a9c"
}'const res = await fetch("https://anytime-capital.com/api/v1/meter-events", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
eventName: "api_call",
customerId: "cus_1042",
value: 1,
idempotencyKey: "req_7f3a9c",
}),
});
const event = await res.json();
if (!res.ok) throw new Error(`${event.error.code}: ${event.error.message}`);import os
import requests
res = requests.post(
"https://anytime-capital.com/api/v1/meter-events",
headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
json={
"eventName": "api_call",
"customerId": "cus_1042",
"value": 1,
"idempotencyKey": "req_7f3a9c",
},
timeout=30,
)
event = res.json()
if not res.ok:
raise RuntimeError(f"{event['error']['code']}: {event['error']['message']}"){
"object": "meter_event",
"livemode": true,
"id": "mev_48213",
"meter": "mtr_8fKq2LmNpR4sT6vW9xYz",
"eventName": "api_call",
"customer": "cus_1042",
"customerEmail": "jordan@example.com",
"value": "1",
"timestamp": "2026-10-24T09:30:00.000Z",
"idempotencyKey": "req_7f3a9c",
"created": "2026-10-24T09:30:01.214Z"
}Errors
- 400
INVALID_JSON - The body isn't valid JSON, or couldn't be read. An empty body reads as
{}. SendContent-Type: application/jsonand a JSON object. - 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged. - 400
CUSTOMER_MISMATCH customerIdandcustomerEmailname different customers. Send one of them.- 404
METER_NOT_FOUND - No active meter has this
eventName. Check the meter's event name on the Meters tab of Subscriptions, or withGET /api/v1/meters. - 404
CUSTOMER_NOT_FOUND - No saved customer has this
customerId. Send the customer'scus_…id, orcustomerEmailinstead. - 409
IDEMPOTENCY_KEY_REUSED - The usage event's
idempotencyKeywas already used for a different event. Send a newidempotencyKeyfor each event; repeat a key only to resend the same event. - 422
CUSTOMER_HAS_NO_EMAIL - The customer has no email address, and usage is matched to subscriptions by email. Add an email to the customer in the dashboard, or send
customerEmail. - 422
TIMESTAMP_IN_FUTURE - The event's
timestampis more than five minutes ahead of our clock. Send the time the usage happened, or leavetimestampout for now. - 422
TIMESTAMP_TOO_OLD - The event's
timestampis more than 400 days ago. Usage that old can't be billed; drop the event. - 422
PERIOD_CLOSED - The
timestampfalls in a period already billed for this customer and meter. Send usage from the end of the billed period on. The message says when that is.
Record usage in a batch
/api/v1/meter-events/batchsubscriptions:writeIdempotent by each event's idempotencyKey.Up to 100 usage events at once, each with its own idempotencyKey. Each is answered by its index: created, duplicate or refused with its own error. The HTTP status is 200 whenever the batch itself is valid: read each event's status. A refused event carries the code a single event would get (METER_NOT_FOUND, PERIOD_CLOSED, IDEMPOTENCY_KEY_REUSED…).
Body
eventsobject[]required- 1–100 items.
events[].eventNamestringrequired- The meter's event name.
events[].customerIdstringcus_…. Send this orcustomerEmail.events[].customerEmailstring- The customer's email. Send this or
customerId. events[].valuenumber or string- The usage, at least 0 with up to 6 decimals. Defaults to 1.
events[].timestampstring (date-time)- ISO 8601 with an offset. Defaults to now; at most 5 minutes ahead.
events[].idempotencyKeystringrequired- Your unique key for this event, 1–255 printable characters.
curl -X POST https://anytime-capital.com/api/v1/meter-events/batch \
-H "Authorization: Bearer $ANYTIME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"eventName": "api_call",
"customerId": "cus_1042",
"idempotencyKey": "req_7f3a9d"
},
{
"eventName": "api_call",
"customerEmail": "jordan@example.com",
"value": 3,
"idempotencyKey": "req_7f3a9e"
}
]
}'const res = await fetch("https://anytime-capital.com/api/v1/meter-events/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
events: [
{
eventName: "api_call",
customerId: "cus_1042",
idempotencyKey: "req_7f3a9d",
},
{
eventName: "api_call",
customerEmail: "jordan@example.com",
value: 3,
idempotencyKey: "req_7f3a9e",
},
],
}),
});
const result = await res.json();
if (!res.ok) throw new Error(`${result.error.code}: ${result.error.message}`);import os
import requests
res = requests.post(
"https://anytime-capital.com/api/v1/meter-events/batch",
headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
json={
"events": [
{
"eventName": "api_call",
"customerId": "cus_1042",
"idempotencyKey": "req_7f3a9d",
},
{
"eventName": "api_call",
"customerEmail": "jordan@example.com",
"value": 3,
"idempotencyKey": "req_7f3a9e",
},
],
},
timeout=30,
)
result = res.json()
if not res.ok:
raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}"){
"object": "list",
"data": [
{
"index": 0,
"status": "created",
"event": {
"object": "meter_event",
"livemode": true,
"id": "mev_48214",
"meter": "mtr_8fKq2LmNpR4sT6vW9xYz",
"eventName": "api_call",
"customer": "cus_1042",
"customerEmail": "jordan@example.com",
"value": "1",
"timestamp": "2026-10-24T09:30:00.000Z",
"idempotencyKey": "req_7f3a9d",
"created": "2026-10-24T09:30:01.214Z"
}
},
{
"index": 1,
"status": "refused",
"error": {
"code": "PERIOD_CLOSED",
"message": "This customer's usage on API calls is already billed up to 2026-10-24T00:00:00.000Z. Send events from then on."
}
}
],
"created": 1,
"duplicates": 0,
"refused": 1
}Errors
- 400
INVALID_JSON - The body isn't valid JSON, or couldn't be read. An empty body reads as
{}. SendContent-Type: application/jsonand a JSON object. - 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged.
List meters
/api/v1/meterssubscriptions:readThe business's usage meters, newest first. Meters are made in the dashboard, on the Meters tab of Subscriptions.
Query
limitinteger- How many objects per page, 1–100. Default 25. See Pagination.
cursorstring- The previous page's
nextCursor, for the page after it.
curl https://anytime-capital.com/api/v1/meters \
-H "Authorization: Bearer $ANYTIME_API_KEY"const res = await fetch("https://anytime-capital.com/api/v1/meters", {
headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
});
const page = await res.json();
if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`);import os
import requests
res = requests.get(
"https://anytime-capital.com/api/v1/meters",
headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
timeout=30,
)
page = res.json()
if not res.ok:
raise RuntimeError(f"{page['error']['code']}: {page['error']['message']}"){
"object": "list",
"data": [
{
"object": "meter",
"livemode": true,
"id": "mtr_8fKq2LmNpR4sT6vW9xYz",
"name": "API calls",
"eventName": "api_call",
"aggregation": "count",
"unitLabel": "API call",
"unitPriceCents": "0.25",
"includedUnits": "1000",
"status": "active",
"createdAt": "2026-10-01T15:04:05.000Z"
}
],
"hasMore": false,
"nextCursor": null
}Errors
- 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged.
Get a customer's usage
/api/v1/meters/{id}/usagesubscriptions:readOne customer's usage on a meter over a window (default the last 30 days, at most 366), with a daily breakdown. 422 CUSTOMER_HAS_NO_EMAIL when the customer has no email to match events by. Usage in [from, to) is added up the meter's way (sum, count, max or last), with one point per UTC day.
Path
idstringrequiredmtr_…
Query
customerIdstringcus_…. Send this orcustomerEmail.customerEmailstring- The customer's email.
fromstring- Window start: a date (
2026-10-01, midnight UTC) or an ISO 8601 date-time. Default: 30 days beforeto. tostring- Window end (exclusive), same format. Default: now. The window spans at most 366 days.
curl "https://anytime-capital.com/api/v1/meters/mtr_8fKq2LmNpR4sT6vW9xYz/usage?customerId=cus_1042&from=2026-10-01&to=2026-10-03" \
-H "Authorization: Bearer $ANYTIME_API_KEY"const res = await fetch("https://anytime-capital.com/api/v1/meters/mtr_8fKq2LmNpR4sT6vW9xYz/usage?customerId=cus_1042&from=2026-10-01&to=2026-10-03", {
headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
});
const usage = await res.json();
if (!res.ok) throw new Error(`${usage.error.code}: ${usage.error.message}`);import os
import requests
res = requests.get(
"https://anytime-capital.com/api/v1/meters/mtr_8fKq2LmNpR4sT6vW9xYz/usage",
headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
params={"customerId": "cus_1042", "from": "2026-10-01", "to": "2026-10-03"},
timeout=30,
)
usage = res.json()
if not res.ok:
raise RuntimeError(f"{usage['error']['code']}: {usage['error']['message']}"){
"object": "meter_usage",
"livemode": true,
"meter": "mtr_8fKq2LmNpR4sT6vW9xYz",
"customer": "cus_1042",
"customerEmail": "jordan@example.com",
"from": "2026-10-01T00:00:00.000Z",
"to": "2026-10-03T00:00:00.000Z",
"aggregation": "count",
"quantity": "1210",
"eventCount": 1210,
"daily": [
{
"date": "2026-10-01",
"quantity": "480"
},
{
"date": "2026-10-02",
"quantity": "730"
}
]
}Errors
- 400
INVALID_INPUT - A field, filter,
limitorcursoris missing or invalid. The message starts with the field's name, as inamountCents: …, or readsInvalid cursor.Fix the named field. Don't retry the same request unchanged. - 400
CUSTOMER_MISMATCH customerIdandcustomerEmailname different customers. Send one of them.- 404
NOT_FOUND - No such object on your business (an id from another business reads the same), or no such endpoint under
/api/v1, which still answers in JSON. Check the id and the path. Ids are case-sensitive. - 422
CUSTOMER_HAS_NO_EMAIL - The customer has no email address, and usage is matched to subscriptions by email. Add an email to the customer in the dashboard, or send
customerEmail.