Swarmz

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.

CardRemaining (numerator)Size (denominator)Cap / footnote
Free dailyderived: 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
Monthlyincluded_remainingincluded_credits (0 ⇒ "not included on this plan")rollover_remaining shown as "+ N rolled over"; else caps.rollover_months as "carry over N mo"
Top-uptopup_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])

CardFieldNotes
Published appslimit = caps.max_published_projectsnull ⇒ unlimited (). The live count is not in this response — show the limit, or pair with your own count.
Custom domainslimit = caps.max_custom_domains, enabled = caps.custom_domains_enablednull limit ⇒ unlimited. When caps.custom_domains_enabled is false, render "not available on this plan" regardless of the limit.
Compute tiercaps.max_compute_sizeThe largest compute size the plan permits (e.g. pico..4xl); null ⇒ unset.
Projectslimit = caps.max_projectsnull ⇒ unlimited.
Cloud budgetcap = caps.cloud_budget_cap (USD)null ⇒ unlimited. Compare against the live usage.cloud_usd to draw a budget bar.
AI spendusage.usd_credits (or usage.by_workspace[i].usd_credits)USD value stamped at charge time, this period. Render as $X.XX.
Cloud spendusage.cloud_usd (or per-workspace cloud_usd)Cloud cost this period. Render as $X.XX.
Credits usedusage.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.
  • null numeric ⇒ . Every caps.* field and the daily allowance use null to mean unlimited — render an infinity glyph, not "0".
  • 0 size ⇒ "not included on this plan." A monthly grant of 0 (or a top-up pool of 0) is a deliberate plan choice, not an error — say so rather than showing a bare "0".
  • Spend is USD, stamped. usd_credits and cloud_usd are dollar figures fixed at charge time; format as currency and do not recompute them from credits_used.
  • Degrade gracefully. If balances is null, still render the spend cards from usage, and fill the pool/cap cards from the plan you assigned (via platform-plans or 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 when balances is null.

On this page