Guide
Credits & metered usage
Sell consumption on top of licensing with Keyright credits: credit pools bound to a license, per-action costs, checking a balance and consuming credits from your app (with dry-run pricing and idempotent retries), and the one-time vendor setup.
Licensing answers “is this customer allowed to run the software?” Credits answer “how much have they used?” — so you can sell renders, API calls, AI generations, exports or any metered action on top of a license, with quotas and overage. Credits are optional: a product that only sells seats never has to touch them.
The model
- Credit pool — a balance of credits bound to a license key. A license can have one or more pools (e.g. a monthly “renders” pool and a top-up “overage” pool), each with a unit name you choose.
- Action cost — what one unit of a metered
actioncosts, in credits. You define costs per product (e.g.render= 1,hd-export= 5). - Consume — your app spends credits for an action; Keyright checks the balance, deducts atomically, and records the usage. Balance reads what’s left.
Consumption uses license-key auth — the same key your app already holds, no admin token. The two calls are online only (there’s no offline fallback); for disconnected machines, see air-gapped credit blocks.
In your app
Every SDK exposes the same two calls; over raw HTTP they’re POST /v1/balance and POST /v1/consume. Two features make them safe in production:
dryRun— price an action without charging, e.g. to show a cost before the user commits.idempotencyKey— a key you supply so a retried call replays the first result instead of charging twice. Always pass one for a real charge, so a network retry never double-bills.
consume returns a status of ok, insufficient (not enough credits), unknown_action (no cost configured), no_pool / ambiguous_pool (no single pool to draw from), or not_found.
// .NET
var pools = await client.BalanceAsync(licenseKey); // remaining balance per pool
var quote = await client.ConsumeAsync(licenseKey, "render", dryRun: true); // price only
var res = await client.ConsumeAsync(licenseKey, "render", quantity: 1, idempotencyKey: txnId);
if (res.Status == "ok") Console.WriteLine($"charged {res.TotalCost} {res.Unit}, {res.Balance} left");
// Node.js
const { pools } = await client.balance(licenseKey);
const quote = await client.consume(licenseKey, 'render', { dryRun: true });
const res = await client.consume(licenseKey, 'render', { quantity: 1, idempotencyKey: txnId });
# Python
pools = client.balance(license_key)
quote = client.consume(license_key, "render", dry_run=True)
res = client.consume(license_key, "render", quantity=1, idempotency_key=txn_id)
// Java
List<PoolBalance> pools = client.balance(licenseKey);
ConsumeResult quote = client.consume(licenseKey, "render", 1, null, true); // dry run
ConsumeResult res = client.consume(licenseKey, "render", 1, txnId, false); // real charge
Any language without an SDK uses the HTTP endpoints directly — see Use the HTTP API directly. Runnable walkthroughs for all of the above (operation 6) are in the SDK samples on GitHub.
One-time vendor setup
Configure costs and fund pools from the admin API (your product admin token) or the dashboard. Price an action, create a pool bound to a license, and grant credits:
SVC=https://keyright.delta1labs.com
# 1. what an action costs
curl -X POST "$SVC/admin/products/<your-product>/action-costs" \
-H "X-Admin-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"action":"render","credits":1}'
# 2. a pool bound to a license key (-> returns {"id":"pool_..."})
curl -X POST "$SVC/admin/credit-pools" \
-H "X-Admin-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"licenseId":"<LICENSE_KEY>","product":"<your-product>","name":"Render credits","unit":"renders"}'
# 3. fund it
curl -X POST "$SVC/admin/credit-pools/<POOL_ID>/grant" \
-H "X-Admin-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"amount":1000}'
Pools also support top-up, transfer, scheduled resets and low-balance thresholds; usage is queryable per pool and across products for reporting. See the HTTP API reference for the full admin surface.
Air-gapped credit blocks
Disconnected machines can still meter: the vendor issues a signed credit block (a fixed allotment the client spends offline), and reconciles the spent amount when the machine next reconnects. This keeps usage-based products working on fully air-gapped deployments. Air-gapped blocks are an entitlement of the higher self-hosted tiers.
FAQ
Do I have to use credits? No. Credits are purely additive — seat-based and perpetual licensing work exactly as before if you never configure a pool.
What happens when a pool runs out? consume returns insufficient and does not deduct. Your app decides whether to block the action, queue it, or prompt the customer to top up (configure an overage pool to allow controlled overage).
Is a charge safe to retry? Yes, when you pass an idempotencyKey. A repeated call with the same key returns the original result and never charges again.