# Record usage events

> The usage_events.create operation: POST /api/v1/usage_events in the REST API and the usage_events_create tool in the MCP server.

Records a batch of 1 to 1,000 usage events for metered prices, billed in arrears on the customer's next
invoice. Each event names the `customerId`, the `meter` (a meter of one of the brand's metered prices), the
`quantity` (an integer string; negative usage, such as refunded volume, reduces the period), when it
happened (`occurredAt`), optional `attributes`, and its own `idempotencyKey`, unique in the brand and mode.

Every event gets a result, in request order (`results[i]`), and the batch is recorded even when some events
fail:

- `created`: recorded now;
- `duplicate`: an event with this key and the same content was recorded earlier (or earlier in the batch);
  it is counted once, so resending a batch after a timeout is safe;
- `failed`: not recorded, with an `error` (`usage_idempotency_key_reused`, `resource_not_found` or
  `customer_deleted` for the customer, `usage_meter_unknown`, `usage_occurred_at_out_of_range`,
  `resource_not_found` or `subscription_item_meter_mismatch` for a `subscriptionItemId`).

A malformed request (a missing field, a quantity that isn't an integer, more than 1,000 events) is refused as a
whole with `validation_failed`, and nothing is recorded.

Usage is aggregated per customer, meter, day (in the entity's time zone) and attribute set. Usage that
happened in a period that was already invoiced is recorded as `late` and billed on the next invoice as a
separate late usage line. Each request counts once against the API key's rate limit, whatever its size.

## At a glance

- **REST:** `POST /api/v1/usage_events`
- **MCP tool:** `usage_events_create`
- **Operation ID:** `usage_events.create`
- **Scope:** Brand
- **Permission:** `usage:write`
- **Risk tier:** `write`
- **Dry run:** Supported (send `Ownrecord-Dry-Run: true`)
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Plan feature:** `usage_billing.enabled`
- **Authentication:** API key with the `usage:write` scope
- **Since:** M5

## Request

### Body

A JSON object (`Content-Type: application/json`):

| Field                         | Type              | Required | Description                                                                                                                                                                                |
| ----------------------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `events`                      | array of object   | Yes      | 1 to 1000 usage events.                                                                                                                                                                    |
| `events[].idempotencyKey`     | string            | Yes      | Unique per event in the brand and mode, e.g. your own event ID. Resending an event with its key is safe: it is counted once.                                                               |
| `events[].customerId`         | `cus_…` ID        | Yes      | The customer whose usage this is (`cus_…`).                                                                                                                                                |
| `events[].meter`              | string            | Yes      | The meter usage events name, e.g. `api.calls` or `payments.volume`.                                                                                                                        |
| `events[].quantity`           | string or integer | Yes      | How much was used, as an integer string (`"3"`, `"-1500000"`); negative usage reduces the period (§16.7). For a percent-of-amount meter it is an amount in micros of the price's currency. |
| `events[].occurredAt`         | date-time         | Yes      | When the usage happened (ISO 8601): at most 5 minutes ahead and three years back.                                                                                                          |
| `events[].attributes`         | object            | No       | Optional dimensions of the event, e.g. `{ "model": "large" }`; usage is aggregated per day and attribute set.                                                                              |
| `events[].subscriptionItemId` | `si_…` ID         | No       | The subscription item to bill (`si_…`); not available yet: usage goes to the customer's meter.                                                                                             |

## Response

`200 OK` with a JSON object:

| Field                      | Type                                    | Description                                                                                                 |
| -------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `object`                   | `"usage_event_batch"`                   |                                                                                                             |
| `created`                  | integer                                 |                                                                                                             |
| `duplicates`               | integer                                 |                                                                                                             |
| `failed`                   | integer                                 |                                                                                                             |
| `results`                  | array of object                         | One result per event, in request order.                                                                     |
| `results[].index`          | integer                                 | The event's position in the request (0-based).                                                              |
| `results[].idempotencyKey` | string                                  |                                                                                                             |
| `results[].status`         | one of `created`, `duplicate`, `failed` | `created`: recorded now; `duplicate`: recorded earlier with this key, counted once; `failed`: not recorded. |
| `results[].id`             | `uev_…` ID or null                      | The recorded event (also for duplicates); null when it failed.                                              |
| `results[].late`           | boolean or null                         | Whether the event is late usage; null when it failed.                                                       |
| `results[].error`          | object or null                          | Why the event wasn't recorded (codes in the error catalogue).                                               |

## Examples

### Two events, one sent again

The second event was already recorded by an earlier request: it is reported, not counted again.

```http
POST /api/v1/usage_events
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "events": [
    {
      "idempotencyKey": "req_7f3a9c",
      "customerId": "cus_01m4ty3c2b5qx7mhd9k8w6s0a1",
      "meter": "api.calls",
      "quantity": "120",
      "occurredAt": "2026-10-13T09:15:00Z",
      "attributes": {
        "model": "large"
      }
    },
    {
      "idempotencyKey": "req_7f3a9b",
      "customerId": "cus_01m4ty3c2b5qx7mhd9k8w6s0a1",
      "meter": "api.calls",
      "quantity": "80",
      "occurredAt": "2026-10-13T09:14:00Z"
    }
  ]
}
```

Response (`200`):

```json
{
  "object": "usage_event_batch",
  "created": 1,
  "duplicates": 1,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "idempotencyKey": "req_7f3a9c",
      "status": "created",
      "id": "uev_01m4v0d9k2j7r5t3w8x6y4z2a0",
      "late": false,
      "error": null
    },
    {
      "index": 1,
      "idempotencyKey": "req_7f3a9b",
      "status": "duplicate",
      "id": "uev_01m4v0d9k2j7r5t3w8x6y4z2a1",
      "late": false,
      "error": null
    }
  ]
}
```

### Refunded volume, and a misspelled meter

```http
POST /api/v1/usage_events
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "events": [
    {
      "idempotencyKey": "refund_re_8812",
      "customerId": "cus_01m4ty3c2b5qx7mhd9k8w6s0a1",
      "meter": "payments.volume",
      "quantity": "-25000000",
      "occurredAt": "2026-10-13T10:00:00Z"
    },
    {
      "idempotencyKey": "call_99102",
      "customerId": "cus_01m4ty3c2b5qx7mhd9k8w6s0a1",
      "meter": "api.cals",
      "quantity": 1,
      "occurredAt": "2026-10-13T10:00:05Z"
    }
  ]
}
```

Response (`200`):

```json
{
  "object": "usage_event_batch",
  "created": 1,
  "duplicates": 0,
  "failed": 1,
  "results": [
    {
      "index": 0,
      "idempotencyKey": "refund_re_8812",
      "status": "created",
      "id": "uev_01m4v0d9k2j7r5t3w8x6y4z2a0",
      "late": false,
      "error": null
    },
    {
      "index": 1,
      "idempotencyKey": "call_99102",
      "status": "failed",
      "id": null,
      "late": null,
      "error": {
        "code": "usage_meter_unknown",
        "message": "No metered price of this brand uses this meter.",
        "param": "events.1.meter",
        "details": {
          "meter": "api.cals"
        }
      }
    }
  ]
}
```

## Errors

| Code                                                                                                                 | HTTP status | When                                                                                                                                    | What to do                                                                                                                                                    |
| -------------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`entitlement_required`](https://ownrecord.io/docs/reference/errors.md#entitlement_required)                         | 403         | The organization's plan doesn't include usage billing (`usage_billing.enabled`).                                                        | Upgrade the organization's plan, or remove something to get back under the limit.                                                                             |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)                               | 400         | The request is malformed: no events or more than 1,000, a missing field, a bad quantity or key (`details.issues`). Nothing is recorded. | Fix the fields listed in `details.issues` and send the request again.                                                                                         |
| [`usage_idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#usage_idempotency_key_reused)         | 409         | Per event: the key was already used for an event with other content.                                                                    | Use a new idempotency key for a new event. Resending the original event unchanged is safe: it is reported as a duplicate and counted once.                    |
| [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)                             | 404         | Per event: no customer with this ID in the brand and mode.                                                                              | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.                                                          |
| [`customer_deleted`](https://ownrecord.io/docs/reference/errors.md#customer_deleted)                                 | 400         | Per event: the customer was deleted.                                                                                                    | Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed.                                   |
| [`usage_meter_unknown`](https://ownrecord.io/docs/reference/errors.md#usage_meter_unknown)                           | 400         | Per event: no metered price of the brand uses the meter.                                                                                | Check the meter key for typos, or create a metered price with this meter first (archived prices count too).                                                   |
| [`usage_occurred_at_out_of_range`](https://ownrecord.io/docs/reference/errors.md#usage_occurred_at_out_of_range)     | 400         | Per event: `occurredAt` is more than 5 minutes ahead or more than three years back.                                                     | Send when the usage happened: at most 5 minutes ahead of now (clock skew) and at most three years back.                                                       |
| [`subscription_item_meter_mismatch`](https://ownrecord.io/docs/reference/errors.md#subscription_item_meter_mismatch) | 400         | Per event: the `subscriptionItemId` isn't the customer's live metered item for this meter.                                              | Name the customer's live metered item for the event's meter, or leave out `subscriptionItemId`: usage is billed to the customer's metered item for the meter. |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)                               | 403         | The credentials can't record this brand's usage (`usage:write`).                                                                        | Ask an organization owner or admin for a role or scope that allows this action.                                                                               |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`approval_required`](https://ownrecord.io/docs/reference/errors.md#approval_required), [`approval_rejected`](https://ownrecord.io/docs/reference/errors.md#approval_rejected), [`approval_expired`](https://ownrecord.io/docs/reference/errors.md#approval_expired), [`stale_approval`](https://ownrecord.io/docs/reference/errors.md#stale_approval), [`approval_policy_denied`](https://ownrecord.io/docs/reference/errors.md#approval_policy_denied), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
