Display usage & stats in your panel
A copy-paste guide to fetching per-tenant usage and standing credit balances from platform-usage and rendering them as a stats panel — the credit pools, plan caps, and per-workspace spend the WHMCS client area shows.
This page shows exactly how to turn one usage call into the stats panel your customer sees in their control panel — the same layout the Swarmz WHMCS module renders in its client area: three separate credit pools, plan caps, and per-tenant spend.
One key-authed POST /platform-usage with a tenant_id returns everything you need in a single round trip: consumption over the period (usage) and the point-in-time standing balance (balances). You do not need any other endpoint to draw the panel.
One call, two halves
usage is consumption over a period (what the tenant spent this month).
balances is the point-in-time standing state (what the tenant has left,
per pool, and the live plan caps). The panel below mixes both: spend cards come
from usage, pool/cap cards come from balances.
Step 1 — fetch one tenant's stats
Call platform-usage with the tenant's tenant_id (the workspace UUID) and a period. Scoping to one tenant makes both usage.by_workspace and balances.by_workspace contain exactly one row — the one you render.
curl -X POST https://api.swarmz.net/functions/v1/platform-usage \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "tenant_id": "<workspace-uuid>", "period": "current_month" }'async function fetchTenantStats(tenantId: string) {
const res = await fetch('https://api.swarmz.net/functions/v1/platform-usage', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SWARMZ_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tenant_id: tenantId, period: 'current_month' }),
});
if (!res.ok) throw new Error(`platform-usage ${res.status}`);
return res.json(); // { ok, usage, balances }
}import os
import requests
def fetch_tenant_stats(tenant_id: str) -> dict:
res = requests.post(
"https://api.swarmz.net/functions/v1/platform-usage",
headers={"Authorization": f"Bearer {os.environ['SWARMZ_API_KEY']}"},
json={"tenant_id": tenant_id, "period": "current_month"},
)
res.raise_for_status()
return res.json() # { ok, usage, balances }The response, scoped to one tenant, looks like this (zeros shown — a fresh tenant returns zeros, not an error):
{
"ok": true,
"usage": {
"credits_used": 0,
"usd_credits": 0,
"cloud_usd": 0,
"period": {
"from": "2026-06-01T00:00:00.000Z",
"to": "2026-07-01T00:00:00.000Z",
"label": "current_month"
},
"by_workspace": [
{ "workspace_id": "<uuid>", "credits_used": 0, "usd_credits": 0, "cloud_usd": 0 }
]
},
"balances": {
"purchased_total": 0,
"topup_available": 0,
"included_remaining": 0,
"rollover_remaining": 0,
"by_workspace": [
{
"workspace_id": "<uuid>",
"purchased_total": 0,
"purchased_consumed": 0,
"purchased_expired": 0,
"topup_available": 0,
"included_credits": 0,
"included_used": 0,
"included_remaining": 0,
"rollover_credits": 0,
"rollover_used": 0,
"rollover_remaining": 0,
"rollover_expiry": null,
"caps": {
"credits_per_day": null,
"monthly_credit_cap": null,
"monthly_credits": null,
"rollover_months": null,
"max_projects": null,
"max_custom_domains": null,
"max_published_projects": null,
"cloud_budget_cap": null
}
}
]
}
}balances may be null
balances is additive and best-effort. If the standing-balance read fails the
endpoint degrades to balances: null rather than failing the whole report.
Render the spend cards from usage regardless, and fall back to the plan caps
you already hold (from platform-plans or the
entitlements you set) for the pool cards. Treat null as "unavailable", not
"zero".
Step 2 — pick out your tenant's rows
When you pass a tenant_id, both by_workspace arrays hold a single entry, but read by workspace_id rather than assuming index 0 so the same code works for an account-wide call too.
function selectTenant(payload: any, tenantId: string) {
const usageRow =
payload.usage?.by_workspace?.find((w: any) => w.workspace_id === tenantId) ?? {
credits_used: 0, usd_credits: 0, cloud_usd: 0,
};
// balances can be null on a read failure — guard it.
const balanceRow =
payload.balances?.by_workspace?.find((w: any) => w.workspace_id === tenantId) ?? null;
return { usageRow, balanceRow, period: payload.usage.period };
}def select_tenant(payload: dict, tenant_id: str):
usage_row = next(
(w for w in payload["usage"]["by_workspace"] if w["workspace_id"] == tenant_id),
{"credits_used": 0, "usd_credits": 0, "cloud_usd": 0},
)
balances = payload.get("balances")
balance_row = None
if balances:
balance_row = next(
(w for w in balances["by_workspace"] if w["workspace_id"] == tenant_id),
None,
)
return usage_row, balance_row, payload["usage"]["period"]Step 3 — map JSON → panel fields
The panel has two rows of cards. The exact mapping the WHMCS client-area template uses:
The three credit pools (from balances.by_workspace[i])
Credits are three separate pools — never one merged number. Each card shows remaining over size, plus the cap that governs it.
| Card | Remaining (numerator) | Size (denominator) | Cap / footnote |
|---|---|---|---|
| Free daily | derived: caps.credits_per_day − used-today (used-today is usually not tracked, so show the allowance) | caps.credits_per_day (null ⇒ render ∞) | caps.monthly_credit_cap as the optional "up to N/month" ceiling; resets 00:00 UTC |
| Monthly | included_remaining | included_credits (0 ⇒ "not included on this plan") | rollover_remaining shown as "+ N rolled over"; else caps.rollover_months as "carry over N mo" |
| Top-up | topup_available | — (top-up has no fixed size; show topup_available alone, or over purchased_total) | "never expires"; purchased_consumed as "N used" |
Free-daily 'used today' is not in this response
platform-usage does not return a per-day consumed counter, so the Free-daily
card shows the allowance (caps.credits_per_day) rather than a live
remaining-today figure. included_* and topup_available ARE live remaining
figures and should be shown as-is. When caps.credits_per_day is null, the
pool is unlimited — render ∞.
Plan caps + spend (from caps and usage.by_workspace[i])
| Card | Field | Notes |
|---|---|---|
| Published apps | limit = caps.max_published_projects | null ⇒ unlimited (∞). The live count is not in this response — show the limit, or pair with your own count. |
| Custom domains | limit = caps.max_custom_domains, enabled = caps.custom_domains_enabled | null limit ⇒ unlimited. When caps.custom_domains_enabled is false, render "not available on this plan" regardless of the limit. |
| Compute tier | caps.max_compute_size | The largest compute size the plan permits (e.g. pico..4xl); null ⇒ unset. |
| Projects | limit = caps.max_projects | null ⇒ unlimited. |
| Cloud budget | cap = caps.cloud_budget_cap (USD) | null ⇒ unlimited. Compare against the live usage.cloud_usd to draw a budget bar. |
| AI spend | usage.usd_credits (or usage.by_workspace[i].usd_credits) | USD value stamped at charge time, this period. Render as $X.XX. |
| Cloud spend | usage.cloud_usd (or per-workspace cloud_usd) | Cloud cost this period. Render as $X.XX. |
| Credits used | usage.credits_used (or per-workspace credits_used) | Count of credits deducted this period. |
caps blends entitlement + billing knobs
Server-side, each caps value prefers the live entitlement and falls back to
the mirrored billing knob (the two are kept in lockstep). You do not need to
reconcile them — read caps.* directly. caps.monthly_credits is the plan's
monthly grant size; included_credits is the current cycle's grant. They
usually match but can differ right after a mid-cycle plan change (see
Credits).
Worked example — render the panel
A complete, framework-free mapping from the JSON into a flat view-model your template can bind to. Mirrors the card set the WHMCS module ships.
// custom-domains on/off is NOT in platform-usage; pass what you set via
// entitlements (or platform-plans) so the panel can show "not available".
function buildPanel(payload: any, tenantId: string, opts: { customDomainsEnabled: boolean }) {
const { usageRow, balanceRow, period } = selectTenant(payload, tenantId);
const caps = balanceRow?.caps ?? {};
const fmtUsd = (n: number) => `$${(n ?? 0).toFixed(2)}`;
const orInf = (n: number | null) => (n == null ? '∞' : String(n));
return {
period: period.label, // 'current_month' | 'last_month' | 'ytd'
pools: {
freeDaily: {
allowance: caps.credits_per_day, // null ⇒ unlimited
display: caps.credits_per_day == null ? '∞' : `${caps.credits_per_day}/day`,
monthlyCeiling: caps.monthly_credit_cap, // optional "up to N/month"
resets: 'daily 00:00 UTC',
},
monthly: {
remaining: balanceRow?.included_remaining ?? null,
size: balanceRow?.included_credits ?? 0, // 0 ⇒ "not included on this plan"
rolledOver: balanceRow?.rollover_remaining ?? 0,
rolloverMonths: caps.rollover_months ?? 0,
rolloverExpiry: balanceRow?.rollover_expiry ?? null,
},
topup: {
available: balanceRow?.topup_available ?? 0,
purchasedTotal: balanceRow?.purchased_total ?? 0,
used: balanceRow?.purchased_consumed ?? 0,
},
},
plan: {
publishedLimit: orInf(caps.max_published_projects),
projectsLimit: orInf(caps.max_projects),
domainsLimit: orInf(caps.max_custom_domains),
customDomainsEnabled: opts.customDomainsEnabled,
cloudBudgetCap: caps.cloud_budget_cap, // null ⇒ unlimited
},
spend: {
creditsUsed: usageRow.credits_used,
aiSpend: fmtUsd(usageRow.usd_credits),
cloudSpend: fmtUsd(usageRow.cloud_usd),
},
// true when balances came back; false ⇒ fall back to your stored plan caps
balancesAvailable: payload.balances != null,
};
}def build_panel(payload: dict, tenant_id: str, custom_domains_enabled: bool) -> dict:
usage_row, balance_row, period = select_tenant(payload, tenant_id)
caps = (balance_row or {}).get("caps", {})
fmt_usd = lambda n: f"${(n or 0):.2f}"
or_inf = lambda n: "∞" if n is None else str(n)
return {
"period": period["label"], # current_month | last_month | ytd
"pools": {
"free_daily": {
"allowance": caps.get("credits_per_day"), # None ⇒ unlimited
"display": "∞" if caps.get("credits_per_day") is None
else f"{caps['credits_per_day']}/day",
"monthly_ceiling": caps.get("monthly_credit_cap"),
"resets": "daily 00:00 UTC",
},
"monthly": {
"remaining": (balance_row or {}).get("included_remaining"),
"size": (balance_row or {}).get("included_credits", 0), # 0 ⇒ not on plan
"rolled_over": (balance_row or {}).get("rollover_remaining", 0),
"rollover_months": caps.get("rollover_months", 0),
"rollover_expiry": (balance_row or {}).get("rollover_expiry"),
},
"topup": {
"available": (balance_row or {}).get("topup_available", 0),
"purchased_total": (balance_row or {}).get("purchased_total", 0),
"used": (balance_row or {}).get("purchased_consumed", 0),
},
},
"plan": {
"published_limit": or_inf(caps.get("max_published_projects")),
"projects_limit": or_inf(caps.get("max_projects")),
"domains_limit": or_inf(caps.get("max_custom_domains")),
"custom_domains_enabled": custom_domains_enabled,
"cloud_budget_cap": caps.get("cloud_budget_cap"), # None ⇒ unlimited
},
"spend": {
"credits_used": usage_row["credits_used"],
"ai_spend": fmt_usd(usage_row["usd_credits"]),
"cloud_spend": fmt_usd(usage_row["cloud_usd"]),
},
"balances_available": payload.get("balances") is not None,
}Display rules to copy
These are the rendering conventions the shipped panel uses — apply them so your numbers always read sensibly:
- Three pools, never merged. Show Free daily, Monthly, and Top-up as separate cards. They are billed differently (see Credits) and merging them hides where a tenant's credits actually come from.
nullnumeric ⇒∞. Everycaps.*field and the daily allowance usenullto mean unlimited — render an infinity glyph, not "0".0size ⇒ "not included on this plan." A monthly grant of0(or a top-up pool of0) is a deliberate plan choice, not an error — say so rather than showing a bare "0".- Spend is USD, stamped.
usd_creditsandcloud_usdare dollar figures fixed at charge time; format as currency and do not recompute them fromcredits_used. - Degrade gracefully. If
balancesisnull, still render the spend cards fromusage, and fill the pool/cap cards from the plan you assigned (viaplatform-plansor the entitlements you set). Show a quiet "couldn't refresh just now" note rather than an empty panel. - Cache for 60s. Usage is a read; cache it for at least a minute instead of refetching on every page view. See Rate limits.
See also
usage— the full field reference for every key in this response.- Credits — what each pool means and exactly when it is billed.
- Entitlements — the caps behind
balances.*.caps. platform-plans— your named plans, for the cap fallback whenbalancesis null.
Usage report
A read-only roll-up of credit and cloud usage across your tenants, for reconciliation. Calendar-aligned, UTC, and safe to call repeatedly.
Account billing summary
The owner-facing billing snapshot for your own platform account — period usage, the upcoming invoice, the card on file, and recent invoices. Supabase user-JWT authed; not a host API call.