Corpland Revenue

Balances

Check before an action, track after it. Both answer from a cache in milliseconds.

balances.check

Also at /v1/check.

Body
{ "customer_id": "user_123", "feature_id": "videos", "required_balance": 1, "entity_id": "ws_1" }
Answer
{
  "allowed": true,
  "customer_id": "user_123",
  "entity_id": "ws_1",
  "feature_id": "videos",
  "required_balance": 1,
  "code": "feature_found",
  "credit_feature_id": null,
  "balance": {
    "feature_id": "videos",
    "type": "metered",
    "unlimited": false,
    "granted": 30,
    "used": 12,
    "remaining": 18,
    "overage_allowed": false,
    "next_reset_at": "2026-11-10T18:00:00.000Z",
    "interval": "month",
    "breakdown": [],
    "usage_limits": [{ "limit": 10, "interval": "day", "usage": 4, "source": "customer" }]
  }
}

code is one of:

CodeMeaning
feature_foundAllowed
insufficient_balanceNot enough left
spend_limit_reached, max_purchase_reachedPay as you go, but past the cap
usage_limit_reachedBalance is left, but the daily, weekly, monthly or yearly cap is used up
no_accessNo plan grants it
feature_not_foundUnknown feature id

When a credit system prices the feature and the customer has no direct balance, credit_feature_id names it and balance is the credit balance. With an entity_id, the entity's own balance is used first and the customer's second.

check never writes, so it is safe to call on page load to grey out a button. Pass "send_event": true to check and track in one call when the action is instant and cannot fail. Pass "with_preview": true and a blocked answer includes preview.upgrade_options, the plans that would unlock it.

Reserve, then settle: locks

For work whose cost you only know at the end, such as an AI completion, reserve the most it could use and settle afterwards:

balances.check
{ "customer_id": "user_123", "feature_id": "credits", "required_balance": 4000, "lock": { "enabled": true, "lock_id": "completion_42", "expires_at": 1760200000000 } }

The amount is held so no other request can spend it. When the work finishes:

balances.finalize
{ "lock_id": "completion_42", "action": "confirm", "override_value": 743 }

confirm keeps the hold, or the override_value if you pass one, and gives the rest back. release gives it all back. A lock that is never finalized is released when it expires, at most 24 hours after it was made.

balances.track

Also at /v1/track.

Body
{ "customer_id": "user_123", "feature_id": "videos", "value": 1, "idempotency_key": "render:vid_42", "properties": { "video_id": "vid_42" } }
Answer
{
  "code": "tracked",
  "customer_id": "user_123",
  "feature_id": "videos",
  "value": 1,
  "applied": 1,
  "credits": 0,
  "capped": false,
  "limit_hit": null,
  "credit_feature_id": null,
  "balance": { "feature_id": "videos", "remaining": 17 }
}
FieldMeaning
valueUnits used. Negative values give units back, such as when a seat is removed. Defaults to 1.
event_nameInstead of feature_id: drains every feature that listens to this event name
entity_idTrack against a seat or workspace
overflowcap (default) stops at the limit and sets capped; reject refuses with a 402; allow goes past it. Pay as you go items go below zero on their own up to their spend limit
appliedUnits actually taken
creditsCredits spent when a credit system paid for it
limit_hitincluded, spend_limit, max_purchase or usage_limit when this call ran into one
timestampOptional ISO time when the usage happened

Always send an idempotency key built from your own record id, so a retried job never counts twice. Tracking can fire balances.limit_reached, balances.usage_alert_triggered and an auto top up; see Limits and alerts.

balances.batch_track

{ "events": [{ "customer_id", "feature_id" | "event_name", "value", "properties", "idempotency_key" }, ...] }, up to 1,000. Answers { "accepted": 998, "failed_count": 2, "failed": [{ "index": 4, "code": "customer_not_found", "message": "..." }] }.

balances.track_tokens

Turns AI tokens into credits by the model's price, and records the provider cost for margin in the same call.

Body
{ "customer_id": "user_123", "model_id": "anthropic/claude-haiku-4-5", "input_tokens": 1200, "output_tokens": 400, "cache_read_tokens": 8000 }

model_id is provider/model as listed on Models.dev. Revenue prices the tokens, adds the credit system's markup, divides by its credit value and tracks the credits. Send cost_usd instead when you already know the cost, such as from OpenRouter. With more than one AI credit system, send feature_id. record_cost: false skips the cost record.

balances.create, balances.update, balances.delete

OperationBodyEffect
balances.create{ "customer_id", "feature_id", "granted": 500, "expires_at"?: "2026-12-31T00:00:00Z", "interval"?: "month", "balance_id"?: "launch_gift" }A grant outside any plan. With interval it resets; with expires_at it disappears then, used before grants that never expire
balances.update{ "customer_id", "feature_id", "usage": 3 } or { ..., "remaining": 40 }Sets the count directly, for counts your app owns such as seats
balances.update{ "customer_id", "feature_id", "balance_id": "balance:launch_gift", "granted": 800 }Changes one grant
balances.delete{ "customer_id", "feature_id", "balance_id" }Removes a grant that is not from a plan

balances.grant ({ "customer_id", "feature_id", "amount", "reason" }) still works and adds a grant that never expires.

balances.list

{ "customer_id": "user_123", "entity_id"? } returns { "customer_id": "user_123", "balances": { "<feature_id>": balance } }.

On this page