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.
/api/v1/usage_eventsRecords 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 anerror(usage_idempotency_key_reused,resource_not_foundorcustomer_deletedfor the customer,usage_meter_unknown,usage_occurred_at_out_of_range,resource_not_foundorsubscription_item_meter_mismatchfor asubscriptionItemId).
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-Keyrequired - Modes: Test and live
- Plan feature:
usage_billing.enabled - Authentication: API key with the
usage:writescope - 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.
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):
{
"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
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):
{
"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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.