Usage API
Programmatically create meters, ingest usage events, query aggregated usage, and manage usage-based pricing
The Usage API lets you programmatically create meters, ingest usage events, query aggregated usage, and configure usage-based pricing. All usage endpoints follow the same V2 API patterns for authentication, idempotency, and error handling.
- How to create and manage meters
- How to ingest usage events (single, batch, and dry-run)
- How to query raw events and aggregated usage
- How to check a customer's current usage balance
- How to configure usage prices with tiered pricing
- Usage-specific webhook events
Authentication and Rate Limits
Usage endpoints require an API key with the appropriate scopes:
| Scope | Access |
|---|---|
usage:read | List meters, query events, get usage balance |
usage:write | Create meters, ingest events, manage prices |
Rate limits: 1,000 requests per minute per merchant (sliding window). Rate limit headers are returned on every response:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window |
X-RateLimit-Remaining | Remaining requests in current window |
X-RateLimit-Reset | Seconds until the window resets |
Base URL: https://api.revkeen.com/v2
All amounts are in minor units (pence for GBP, cents for USD).
Meters API
List Meters
GET /v2/metersQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
limit | integer | Results per page (default: 20, max: 100) |
offset | integer | Number of results to skip |
status | string | Filter by status: active, archived, draft |
const meters = await revkeen.meters.list({ status: 'active' });
for (const meter of meters.data) {
console.log(`${meter.name} (${meter.aggregation}): ${meter.event_name}`);
}meters = client.meters.list(status="active")
for meter in meters.data:
print(f"{meter.name} ({meter.aggregation}): {meter.event_name}")curl -X GET "https://api.revkeen.com/v2/meters?status=active" \
-H "x-api-key: rk_live_your_api_key"Response:
{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "API Calls",
"slug": "api-calls",
"event_name": "api_call",
"aggregation": "count",
"value_key": null,
"unique_count_key": null,
"unit_name": "calls",
"description": "Tracks API calls per customer",
"status": "active",
"filter_conditions": [],
"carry_forward": false,
"metadata": {},
"created_at": "2026-03-01T00:00:00Z",
"updated_at": "2026-03-01T00:00:00Z"
}
],
"meta": { "total": 1, "limit": 20, "offset": 0 }
}Get Meter
GET /v2/meters/{meterId}Returns a single meter with all fields including filter conditions and configuration.
const meter = await revkeen.meters.get('mtr_xxxxxxxx');Create Meter
POST /v2/metersRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
event_name | string | Yes | Event name to match |
aggregation | enum | Yes | sum, count, count_unique, max, last |
slug | string | No | URL-friendly identifier (unique per merchant) |
description | string | No | Human-readable description |
value_key | string | No | Property key for sum/max/last (required for those types) |
unique_count_key | string | No | Property key for count_unique (required for that type) |
unit_name | string | No | Display unit (e.g., "calls", "GB") |
filter_conditions | array | No | Event filter conditions |
carry_forward | boolean | No | Carry last value into next period (default: false) |
metadata | object | No | Custom key-value data |
alert_thresholds | number[] | No | Threshold percentages that trigger usage.threshold.reached webhooks (max 5, range 1-200). Example: [50, 80, 100] |
const meter = await revkeen.meters.create({
name: 'Storage Used',
event_name: 'storage_report',
aggregation: 'last',
value_key: 'gb_used',
unit_name: 'GB',
slug: 'storage',
});meter = client.meters.create(
name="Storage Used",
event_name="storage_report",
aggregation="last",
value_key="gb_used",
unit_name="GB",
slug="storage",
)curl -X POST "https://api.revkeen.com/v2/meters" \
-H "x-api-key: rk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Storage Used",
"event_name": "storage_report",
"aggregation": "last",
"value_key": "gb_used",
"unit_name": "GB",
"slug": "storage"
}'Update Meter
PATCH /v2/meters/{meterId}Updatable Fields:
| Field | Type | Description |
|---|---|---|
name | string | Display name |
description | string | Description |
status | enum | active, archived, draft |
unit_name | string | Display unit |
metadata | object | Custom key-value data |
alert_thresholds | number[] | Threshold percentages for usage.threshold.reached webhooks |
The event_name and aggregation settings are immutable after creation. Create a new meter if you need to change these.
await revkeen.meters.update('mtr_xxxxxxxx', {
name: 'API Calls (v2)',
status: 'archived',
});Meters cannot be deleted -- only archived via PATCH. This preserves historical usage data and audit trails.
Usage Events API
Ingest Events
POST /v2/usage-eventsIngest one or more usage events. Accepts a single event or an array of up to 1,000 events.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
events | array | Yes | Array of event objects (max 1,000) |
events[].name | string | Yes | Event name matching a meter's event_name |
events[].customer_id | string | No | RevKeen customer ID |
events[].external_customer_id | string | No | Your system's customer ID |
events[].subscription_id | string | No | Associate with a specific subscription |
events[].meter_id | string | No | Target a specific meter |
events[].quantity | number | No | Numeric value (default: 1) |
events[].timestamp | ISO 8601 | No | When the event occurred (default: now) |
events[].idempotency_key | string | No | Unique key for safe retries |
events[].metadata | object | No | Custom properties |
await revkeen.usageEvents.ingest({
events: [{
name: 'api_call',
customer_id: 'cus_xxxxxxxx',
quantity: 1,
idempotency_key: 'evt_20260315_abc123',
metadata: { endpoint: '/v2/users', region: 'eu-west-1' },
}],
});client.usage_events.ingest(events=[{
"name": "api_call",
"customer_id": "cus_xxxxxxxx",
"quantity": 1,
"idempotency_key": "evt_20260315_abc123",
"metadata": {"endpoint": "/v2/users", "region": "eu-west-1"},
}])curl -X POST "https://api.revkeen.com/v2/usage-events" \
-H "x-api-key: rk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"name": "api_call",
"customer_id": "cus_xxxxxxxx",
"quantity": 1,
"idempotency_key": "evt_20260315_abc123",
"metadata": { "endpoint": "/v2/users" }
}]
}'Idempotency: Sending the same idempotency_key twice returns 409 Conflict. Keys are scoped to the merchant.
Batch Ingest
POST /v2/usage-events/batchLegacy alias for the main ingest endpoint. Accepts the same payload format. Both endpoints accept arrays of up to 1,000 events.
Dry Run
POST /v2/usage-events/dry-runValidate events without persisting them. Returns detailed validation results for each event.
const result = await revkeen.usageEvents.dryRun({
events: [
{ name: 'api_call', customer_id: 'cus_xxxxxxxx', quantity: 1 },
{ name: 'unknown_event', customer_id: 'cus_xxxxxxxx', quantity: 1 },
],
});
console.log(result.summary);
// { would_ingest: 1, would_skip: 0, would_fail: 1 }
for (const item of result.data) {
console.log(`Event ${item.index}: ${item.status}`);
if (item.validationDetails) {
console.log(` Meter matched: ${item.validationDetails.meterMatched}`);
console.log(` Customer matched: ${item.validationDetails.customerMatched}`);
console.log(` Filters passed: ${item.validationDetails.filtersPassed}`);
}
}result = client.usage_events.dry_run(events=[
{"name": "api_call", "customer_id": "cus_xxxxxxxx", "quantity": 1},
{"name": "unknown_event", "customer_id": "cus_xxxxxxxx", "quantity": 1},
])
print(result.summary)
# {"would_ingest": 1, "would_skip": 0, "would_fail": 1}Response:
{
"object": "usage_event_dry_run_result",
"summary": {
"would_ingest": 1,
"would_skip": 0,
"would_fail": 1
},
"data": [
{
"index": 0,
"status": "would_ingest",
"validationDetails": {
"meterMatched": true,
"customerMatched": true,
"billableQuantity": 1,
"filtersPassed": true
}
},
{
"index": 1,
"status": "would_fail",
"reason": "No meter found for event name: unknown_event"
}
]
}Query Events
GET /v2/usage-eventsQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
meter_id | UUID | Filter by meter |
customer_id | UUID | Filter by customer |
subscription_id | UUID | Filter by subscription |
start_date | ISO 8601 | Start of time range |
end_date | ISO 8601 | End of time range |
page | integer | Page number |
limit | integer | Results per page |
Returns raw event records (not aggregated).
const events = await revkeen.usageEvents.list({
meter_id: 'mtr_xxxxxxxx',
customer_id: 'cus_xxxxxxxx',
start_date: '2026-03-01T00:00:00Z',
end_date: '2026-03-31T23:59:59Z',
limit: 50,
});events = client.usage_events.list(
meter_id="mtr_xxxxxxxx",
customer_id="cus_xxxxxxxx",
start_date="2026-03-01T00:00:00Z",
end_date="2026-03-31T23:59:59Z",
limit=50,
)Aggregate Usage
GET /v2/usage-events/aggregate/{meterId}Returns aggregated usage values based on the meter's aggregation type.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
customer_id | UUID | Filter by customer |
start_date | ISO 8601 | Start of time range (required) |
end_date | ISO 8601 | End of time range (required) |
group_by | string | Group results by a property |
const aggregate = await revkeen.usageEvents.aggregate('mtr_xxxxxxxx', {
customer_id: 'cus_xxxxxxxx',
start_date: '2026-03-01T00:00:00Z',
end_date: '2026-03-31T23:59:59Z',
});
console.log(`Total usage: ${aggregate.value}`);aggregate = client.usage_events.aggregate(
"mtr_xxxxxxxx",
customer_id="cus_xxxxxxxx",
start_date="2026-03-01T00:00:00Z",
end_date="2026-03-31T23:59:59Z",
)
print(f"Total usage: {aggregate.value}")Usage Balance API
Get Customer Balance
GET /v2/usage-balanceReturns current-period usage with estimated costs. Use this to build customer-facing usage dashboards.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
meter_id | UUID | Filter by specific meter |
customer_id | UUID | Filter by customer |
subscription_id | UUID | Filter by subscription |
const balance = await revkeen.usageBalance.get({
customer_id: 'cus_xxxxxxxx',
});
for (const meter of balance.meters) {
console.log(`${meter.meter_name}: ${meter.current_value} ${meter.unit_name}`);
console.log(` Included: ${meter.included_quantity}`);
console.log(` Overage: ${meter.overage_quantity}`);
console.log(` Estimated charge: £${meter.estimated_amount_minor / 100}`);
}
console.log(`Total estimated: £${balance.total_estimated_amount_minor / 100}`);balance = client.usage_balance.get(customer_id="cus_xxxxxxxx")
for meter in balance.meters:
print(f"{meter.meter_name}: {meter.current_value} {meter.unit_name}")
print(f" Estimated charge: £{meter.estimated_amount_minor / 100}")
print(f"Total estimated: £{balance.total_estimated_amount_minor / 100}")curl -X GET "https://api.revkeen.com/v2/usage-balance?customer_id=cus_xxxxxxxx" \
-H "x-api-key: rk_live_your_api_key"Response:
{
"object": "usage_balance",
"meters": [
{
"meter_id": "mtr_xxxxxxxx",
"meter_name": "API Calls",
"unit_name": "calls",
"current_value": 8500,
"included_quantity": 1000,
"used_quantity": 8500,
"remaining_included": 0,
"overage_quantity": 7500,
"estimated_amount_minor": 7500,
"total_cost_minor": null,
"margin_minor": null,
"currency": "GBP",
"period_start": "2026-03-01T00:00:00Z",
"period_end": "2026-03-31T23:59:59Z"
}
],
"total_estimated_amount_minor": 7500,
"total_cost_minor": null,
"currency": "GBP"
}Usage Prices API
Create Usage Price
POST /v2/meters/{meterId}/pricesAttach a usage price to a meter with a specific pricing model.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
pricing_model | enum | Yes | per_unit, graduated, volume, package |
currency | string | Yes | ISO 4217 currency code |
unit_amount_minor | integer | No | Price per unit in minor units (for per_unit) |
flat_fee_minor | integer | No | Base charge per period |
package_size | integer | No | Units per package (for package model) |
tiers | array | No | Tier configuration (for graduated and volume) |
tiers[].first_unit | integer | Yes | Start of tier range |
tiers[].last_unit | integer | No | End of tier range (null for unbounded) |
tiers[].unit_amount_minor | integer | Yes | Per-unit price in this tier |
tiers[].flat_amount_minor | integer | No | Flat fee for entering this tier |
cost_per_unit_minor | integer | No | Merchant's cost per unit for margin intelligence (stored in metadata) |
Per-Unit Example
await revkeen.meters.createPrice('mtr_xxxxxxxx', {
pricing_model: 'per_unit',
unit_amount_minor: 1, // £0.01 per unit
currency: 'GBP',
});Graduated Tiers Example
await revkeen.meters.createPrice('mtr_xxxxxxxx', {
pricing_model: 'graduated',
currency: 'GBP',
tiers: [
{ first_unit: 1, last_unit: 1000, unit_amount_minor: 10 }, // £0.10 each
{ first_unit: 1001, last_unit: 10000, unit_amount_minor: 8 }, // £0.08 each
{ first_unit: 10001, last_unit: null, unit_amount_minor: 5 }, // £0.05 each
],
});Volume Tiers Example
await revkeen.meters.createPrice('mtr_xxxxxxxx', {
pricing_model: 'volume',
currency: 'GBP',
tiers: [
{ first_unit: 1, last_unit: 1000, unit_amount_minor: 10 },
{ first_unit: 1001, last_unit: 10000, unit_amount_minor: 8 },
{ first_unit: 10001, last_unit: null, unit_amount_minor: 5 },
],
});Package Example
await revkeen.meters.createPrice('mtr_xxxxxxxx', {
pricing_model: 'package',
package_size: 100,
unit_amount_minor: 500, // £5.00 per 100 units
currency: 'GBP',
});Flat Fee + Usage Example
await revkeen.meters.createPrice('mtr_xxxxxxxx', {
pricing_model: 'per_unit',
unit_amount_minor: 5, // £0.05 per message
flat_fee_minor: 2000, // £20.00 base charge
currency: 'GBP',
});List Usage Prices
GET /v2/meters/{meterId}/pricesReturns all prices attached to a meter.
Update Usage Price
PATCH /v2/meters/{meterId}/prices/{priceId}Update an existing usage price configuration. The pricing_model is immutable — create a new price if you need to change models.
Deactivate Usage Price
POST /v2/meters/{meterId}/prices/{priceId}/deactivateDeactivate a usage price. Deactivated prices cannot be used for new subscriptions but existing subscriptions continue until the end of their billing period. Prices cannot be deleted — only deactivated.
await revkeen.meters.deactivatePrice('mtr_xxxxxxxx', 'price_yyyyyyyy');End-to-End Billing Flow
The complete usage-based billing lifecycle:
1. Create Meter → POST /v2/meters
2. Attach Price → POST /v2/meters/{meterId}/prices
3. Create Subscription → POST /v2/subscriptions (with metered item)
4. Ingest Events → POST /v2/usage-events
5. Check Balance → GET /v2/usage/balance
6. Period Finalizes → usage.period_finalized webhook
7. Invoice Generated → invoice.created webhook (billing_reason: "usage")
8. Payment Collected → payment.succeeded webhookUsage invoices are identified by billing_reason: "usage" on the standard invoice.created webhook — there is no separate usage.invoice.created event.
Webhook Events
Subscribe to usage-specific webhook events:
| Event | Description |
|---|---|
usage.threshold.reached | Meter usage exceeds a configured alert_thresholds percentage |
usage.period_finalized | A billing period has been finalized with final quantities |
usage.event.rejected | Event rejected during ingestion (synchronous validation) or by ClickHouse (asynchronous, ~60s delay) |
Usage invoices use the standard invoice.created webhook with billing_reason: "usage". There is no separate usage.invoice.created event.
Event Payloads
usage.threshold.reached
{
"id": "evt_xxxxxxxx",
"type": "usage.threshold.reached",
"created": "2026-03-15T14:30:00Z",
"data": {
"meter_id": "mtr_xxxxxxxx",
"customer_id": "cus_xxxxxxxx",
"threshold": 80,
"current_usage": 8500,
"included_quantity": 10000
}
}usage.period_finalized
{
"id": "evt_xxxxxxxx",
"type": "usage.period_finalized",
"created": "2026-03-31T23:59:59Z",
"data": {
"record_id": "rec_xxxxxxxx",
"meter_id": "mtr_xxxxxxxx",
"customer_id": "cus_xxxxxxxx",
"quantity": 15000,
"period_start": "2026-03-01T00:00:00Z",
"period_end": "2026-03-31T23:59:59Z"
}
}usage.event.rejected
{
"id": "evt_xxxxxxxx",
"type": "usage.event.rejected",
"created": "2026-03-15T10:00:00Z",
"data": {
"meter_id": "mtr_xxxxxxxx",
"event_name": "api_call",
"reason": "Customer not found: cus_invalid",
"original_payload": {
"name": "api_call",
"customer_id": "cus_invalid",
"quantity": 1
}
}
}Two-stage rejection: Events can be rejected at two points:
- Synchronous (HTTP response) — validation failures during
POST /v2/usage-eventsreturn207 Multi-Statuswith per-eventstatus: "failed"andreason. - Asynchronous (~60s delay) — malformed messages that pass HTTP validation but fail ClickHouse parsing are routed to an error table and emit
usage.event.rejectedwebhooks.
Webhook Handler Example
app.post('/webhooks/revkeen', async (req, res) => {
const event = req.body;
switch (event.type) {
case 'usage.threshold.reached':
await alertCustomer(event.data.customer_id, event.data.threshold);
break;
case 'usage.period_finalized':
console.log(`Period finalized: ${event.data.quantity} units`);
break;
case 'usage.event.rejected':
console.error(`Event rejected: ${event.data.reason}`);
break;
}
res.json({ received: true });
});Error Codes
| HTTP Status | Meaning | Common Cause |
|---|---|---|
400 | Invalid request | Malformed event schema, missing required fields |
404 | Not found | Meter or customer doesn't exist |
409 | Conflict | Duplicate idempotency key (event already ingested) |
422 | Unprocessable | Cannot modify finalized record, invalid pricing config |
429 | Rate limited | Exceeded 1,000 requests per minute |