RevKeen Docs

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.

What you'll learn
  • 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:

ScopeAccess
usage:readList meters, query events, get usage balance
usage:writeCreate meters, ingest events, manage prices

Rate limits: 1,000 requests per minute per merchant (sliding window). Rate limit headers are returned on every response:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetSeconds 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/meters

Query Parameters:

ParameterTypeDescription
limitintegerResults per page (default: 20, max: 100)
offsetintegerNumber of results to skip
statusstringFilter 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/meters

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name
event_namestringYesEvent name to match
aggregationenumYessum, count, count_unique, max, last
slugstringNoURL-friendly identifier (unique per merchant)
descriptionstringNoHuman-readable description
value_keystringNoProperty key for sum/max/last (required for those types)
unique_count_keystringNoProperty key for count_unique (required for that type)
unit_namestringNoDisplay unit (e.g., "calls", "GB")
filter_conditionsarrayNoEvent filter conditions
carry_forwardbooleanNoCarry last value into next period (default: false)
metadataobjectNoCustom key-value data
alert_thresholdsnumber[]NoThreshold 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:

FieldTypeDescription
namestringDisplay name
descriptionstringDescription
statusenumactive, archived, draft
unit_namestringDisplay unit
metadataobjectCustom key-value data
alert_thresholdsnumber[]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-events

Ingest one or more usage events. Accepts a single event or an array of up to 1,000 events.

Request Body:

FieldTypeRequiredDescription
eventsarrayYesArray of event objects (max 1,000)
events[].namestringYesEvent name matching a meter's event_name
events[].customer_idstringNoRevKeen customer ID
events[].external_customer_idstringNoYour system's customer ID
events[].subscription_idstringNoAssociate with a specific subscription
events[].meter_idstringNoTarget a specific meter
events[].quantitynumberNoNumeric value (default: 1)
events[].timestampISO 8601NoWhen the event occurred (default: now)
events[].idempotency_keystringNoUnique key for safe retries
events[].metadataobjectNoCustom 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/batch

Legacy 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-run

Validate 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-events

Query Parameters:

ParameterTypeDescription
meter_idUUIDFilter by meter
customer_idUUIDFilter by customer
subscription_idUUIDFilter by subscription
start_dateISO 8601Start of time range
end_dateISO 8601End of time range
pageintegerPage number
limitintegerResults 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:

ParameterTypeDescription
customer_idUUIDFilter by customer
start_dateISO 8601Start of time range (required)
end_dateISO 8601End of time range (required)
group_bystringGroup 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-balance

Returns current-period usage with estimated costs. Use this to build customer-facing usage dashboards.

Query Parameters:

ParameterTypeDescription
meter_idUUIDFilter by specific meter
customer_idUUIDFilter by customer
subscription_idUUIDFilter 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}/prices

Attach a usage price to a meter with a specific pricing model.

Request Body:

FieldTypeRequiredDescription
pricing_modelenumYesper_unit, graduated, volume, package
currencystringYesISO 4217 currency code
unit_amount_minorintegerNoPrice per unit in minor units (for per_unit)
flat_fee_minorintegerNoBase charge per period
package_sizeintegerNoUnits per package (for package model)
tiersarrayNoTier configuration (for graduated and volume)
tiers[].first_unitintegerYesStart of tier range
tiers[].last_unitintegerNoEnd of tier range (null for unbounded)
tiers[].unit_amount_minorintegerYesPer-unit price in this tier
tiers[].flat_amount_minorintegerNoFlat fee for entering this tier
cost_per_unit_minorintegerNoMerchant'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}/prices

Returns 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}/deactivate

Deactivate 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 webhook

Usage 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:

EventDescription
usage.threshold.reachedMeter usage exceeds a configured alert_thresholds percentage
usage.period_finalizedA billing period has been finalized with final quantities
usage.event.rejectedEvent 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:

  1. Synchronous (HTTP response) — validation failures during POST /v2/usage-events return 207 Multi-Status with per-event status: "failed" and reason.
  2. Asynchronous (~60s delay) — malformed messages that pass HTTP validation but fail ClickHouse parsing are routed to an error table and emit usage.event.rejected webhooks.

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 StatusMeaningCommon Cause
400Invalid requestMalformed event schema, missing required fields
404Not foundMeter or customer doesn't exist
409ConflictDuplicate idempotency key (event already ingested)
422UnprocessableCannot modify finalized record, invalid pricing config
429Rate limitedExceeded 1,000 requests per minute

On this page