# Meters and usage events API

> Send usage events to Anytime Capital meters one at a time or in batches of 100, list your meters and read a customer's usage, for usage-based billing.

Source: https://anytime-capital.com/docs/meters · Markdown: https://anytime-capital.com/docs/meters.md · All docs: https://anytime-capital.com/docs/llms-full.txt

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](https://anytime-capital.com/docs/usage-billing.md); a complete sender is in the [usage recipe](https://anytime-capital.com/docs/bill-usage-with-meters.md). Reading needs `subscriptions:read`, sending `subscriptions:write`.

| Topic | Detail |
| --- | --- |
| Endpoints | [`POST /api/v1/meter-events`](#create-meter-event), [`POST /api/v1/meter-events/batch`](#create-meter-event-batch), [`GET /api/v1/meters`](#list-meters), [`GET /api/v1/meters/{id}/usage`](#get-meter-usage) |
| Key permissions | `subscriptions:write`, `subscriptions:read` |
| Samples | curl; Node.js 18+ with the built-in `fetch` (in an ES module); the [Node SDK](https://anytime-capital.com/docs/node-sdk.md) (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](https://anytime-capital.com/docs/error-codes.md). |

> **Idempotency is per event:** Usage events don't use the `Idempotency-Key` header. Each event carries its own `idempotencyKey` in the body (your request id, say), remembered for good: sending the same event again answers `200` with the event already recorded, so retries never count twice.

## The meter object

```json
{
  "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

```json
{
  "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

```json
{
  "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

`POST /api/v1/meter-events`

Permission: `subscriptions:write`. Idempotent 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**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `eventName` | string | yes | The meter's event name. |
| `customerId` | string | no | `cus_…`. Send this or `customerEmail`. |
| `customerEmail` | string | no | The customer's email. Send this or `customerId`. |
| `value` | number or string | no | The usage, at least 0 with up to 6 decimals. Defaults to 1. |
| `timestamp` | string (date-time) | no | ISO 8601 with an offset. Defaults to now; at most 5 minutes ahead. |
| `idempotencyKey` | string | yes | 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 `200` with the event already recorded; nothing is counted twice. |

**Request (curl)**

```bash
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"
  }'
```

**Request (Node.js)**

```javascript
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}`);
```

**Request (Python)**

```python
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']}")
```

**Response** (201)

```json
{
  "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**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_JSON` | The body isn't valid JSON, or couldn't be read. An empty body reads as `{}`. Send `Content-Type: application/json` and a JSON object. |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |
| 400 | `CUSTOMER_MISMATCH` | `customerId` and `customerEmail` name 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 with `GET /api/v1/meters`. |
| 404 | `CUSTOMER_NOT_FOUND` | No saved customer has this `customerId`. Send the customer's `cus_…` id, or `customerEmail` instead. |
| 409 | `IDEMPOTENCY_KEY_REUSED` | The usage event's `idempotencyKey` was already used for a different event. Send a new `idempotencyKey` for 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 `timestamp` is more than five minutes ahead of our clock. Send the time the usage happened, or leave `timestamp` out for now. |
| 422 | `TIMESTAMP_TOO_OLD` | The event's `timestamp` is more than 400 days ago. Usage that old can't be billed; drop the event. |
| 422 | `PERIOD_CLOSED` | The `timestamp` falls 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

`POST /api/v1/meter-events/batch`

Permission: `subscriptions:write`. Idempotent 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**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `events` | object[] | yes | 1–100 items. |
| `events[].eventName` | string | yes | The meter's event name. |
| `events[].customerId` | string | no | `cus_…`. Send this or `customerEmail`. |
| `events[].customerEmail` | string | no | The customer's email. Send this or `customerId`. |
| `events[].value` | number or string | no | The usage, at least 0 with up to 6 decimals. Defaults to 1. |
| `events[].timestamp` | string (date-time) | no | ISO 8601 with an offset. Defaults to now; at most 5 minutes ahead. |
| `events[].idempotencyKey` | string | yes | Your unique key for this event, 1–255 printable characters. |

**Request (curl)**

```bash
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"
      }
    ]
  }'
```

**Request (Node.js)**

```javascript
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}`);
```

**Request (Python)**

```python
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']}")
```

**Response** (200)

```json
{
  "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**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_JSON` | The body isn't valid JSON, or couldn't be read. An empty body reads as `{}`. Send `Content-Type: application/json` and a JSON object. |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |

### List meters

`GET /api/v1/meters`

Permission: `subscriptions:read`.

The business's usage meters, newest first. Meters are made in the dashboard, on the **Meters** tab of **Subscriptions**.

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | How many objects per page, 1–100. Default 25. See [Pagination](https://anytime-capital.com/docs/pagination.md). |
| `cursor` | string | no | The previous page's `nextCursor`, for the page after it. |

**Request (curl)**

```bash
curl https://anytime-capital.com/api/v1/meters \
  -H "Authorization: Bearer $ANYTIME_API_KEY"
```

**Request (Node.js)**

```javascript
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}`);
```

**Request (Python)**

```python
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']}")
```

**Response** (200)

```json
{
  "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**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |

### Get a customer's usage

`GET /api/v1/meters/{id}/usage`

Permission: `subscriptions:read`.

One 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**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | `mtr_…` |

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `customerId` | string | no | `cus_…`. Send this or `customerEmail`. |
| `customerEmail` | string | no | The customer's email. |
| `from` | string | no | Window start: a date (`2026-10-01`, midnight UTC) or an ISO 8601 date-time. Default: 30 days before `to`. |
| `to` | string | no | Window end (exclusive), same format. Default: now. The window spans at most 366 days. |

**Request (curl)**

```bash
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"
```

**Request (Node.js)**

```javascript
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}`);
```

**Request (Python)**

```python
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']}")
```

**Response** (200)

```json
{
  "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**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |
| 400 | `CUSTOMER_MISMATCH` | `customerId` and `customerEmail` name 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`. |

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
