Usage & Entitlements
Two endpoints connect your product to the subscription engine at runtime: usage/record reports consumption of a metered feature (billing), and usage/check-entitlement asks whether a customer may use a feature right now (enforcement). Together with the feature types you get metered billing, hard quotas and boolean gates from the same model.
Both endpoints use your secret key as a Bearer token under https://api-gateway.lunixi.com/api/v1/subscriptions.
QUOTA vs METERED_USAGE
QUOTA | METERED_USAGE | |
|---|---|---|
| Question it answers | "May this customer still do X?" | "How much X did they do?" |
| Runtime call | usage/check-entitlement before the action | usage/record after the action |
| When it stops the customer | When the allowance is used up (check fails) | Never — it meters, billing reflects it |
| Resets | Every resetPeriodDays on the feature | Counter window resets per resetPeriodDays |
BOOLEAN for comparison | check-entitlement returns allowed/denied purely by plan | — |
For a capped-but-billable feature, combine them: check the entitlement first, and record the usage once the action succeeds.
Record usage (metered billing)
POST /subscriptions/usage/record — report consumption for a METERED_USAGE (or windowed QUOTA) feature. idempotencyKey deduplicates: retrying the same request never double-counts.
curl -X POST https://api-gateway.lunixi.com/api/v1/subscriptions/usage/record \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-H "Content-Type: application/json" \
-d '{
"subscriptionId": "sub_01JX5B8R3M",
"featureCode": "api-calls",
"quantity": 25,
"idempotencyKey": "req-7f3a91c2"
}'Each accepted record emits a usage.recorded webhook. Recorded quantities are rated onto the subscription's period invoice.
Derive idempotencyKey from the business event you are metering (your request id, job id, message id) — not from a random value per attempt. That is what makes retries safe: same event, same key, counted once.
Check an entitlement (quota enforcement)
POST /subscriptions/usage/check-entitlement — ask, by customer, whether a feature may be used right now. Works for BOOLEAN (has it / doesn't) and QUOTA (allowance left / used up) features.
curl -X POST https://api-gateway.lunixi.com/api/v1/subscriptions/usage/check-entitlement \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust_01JX2F9K4T",
"featureCode": "reports-export"
}'{
"allowed": true,
"featureCode": "reports-export"
}Gate the action on allowed. A denial means the customer's plan doesn't include the feature, or a QUOTA allowance is exhausted for the current window.
Integration pattern: gate every call, record after success
The standard shape for a capped, metered API feature:
Check before the action
Call check-entitlement with the customer and feature. If allowed is false, reject with your own upgrade prompt (HTTP 402/403 in your API).
Do the work
Run the action only after the entitlement passed.
Record after success
Call usage/record with an idempotency key derived from the request — never record usage for work that failed.
// Express-style middleware + handler
async function requireFeature(req, res, next) {
const r = await fetch(
'https://api-gateway.lunixi.com/api/v1/subscriptions/usage/check-entitlement',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNIXI_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerId: req.customer.lunixiId,
featureCode: 'api-calls',
}),
},
);
const { allowed } = await r.json();
if (!allowed) return res.status(402).json({ error: 'quota_exceeded' });
next();
}
app.post('/v1/reports', requireFeature, async (req, res) => {
const report = await generateReport(req.body); // the actual work
await fetch('https://api-gateway.lunixi.com/api/v1/subscriptions/usage/record', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LUNIXI_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
subscriptionId: req.customer.subscriptionId,
featureCode: 'api-calls',
quantity: 1,
idempotencyKey: `report-${report.id}`, // business-derived key
}),
});
res.json(report);
});For high-throughput paths, batch on your side: aggregate counts per subscription over a short window and send one usage/record with the summed quantity and a window-derived idempotencyKey (e.g. api-calls-sub_x-2026-07-06T14:05). Fewer calls, same billing outcome.
Errors
| Status | Meaning | What to do |
|---|---|---|
400 | Validation failed (missing idempotencyKey, non-positive quantity) | Fix the body |
401 | Missing/invalid Bearer key | Check the Authorization header |
404 | Unknown subscriptionId, customerId or featureCode | Verify ids; feature codes must match the feature definition |
409 | Idempotency conflict — same key, different payload | Reuse a key only for the exact same event |
422 | Feature type doesn't support the operation (e.g. recording usage against a BOOLEAN feature) | Check the feature's type |
Webhooks
| Event | When |
|---|---|
usage.recorded | A usage record was accepted |
invoice.created | The period invoice carrying rated usage was generated |
See Webhooks for delivery and signature verification.
Next steps
- Features & Add-ons — define the features you meter and gate
- Plans — attach features and allowances to plans
- Invoices — where metered usage is billed