Skip to main content

Billing Dashboard Implementation

Overview​

This guide explains how to implement a tenant-based real-time billing dashboard by combining the SaaSus Metering API and Pricing API, based on the sample app Billing Dashboard.

Screenshot of the billing dashboard:

Frontend Implementation​

Example Implementations​

The following links point to repositories that include implementations of the frontend.

Backend Implementation​

Endpoint Summary​

CategoryMethod & PathDescription
Plan Period CandidatesGET /billing/plan_periods?tenant_id=Returns monthly or yearly periods based on the tenant's plan history. Used for generating a select box.
Dashboard AggregationGET /billing/dashboardRetrieves metering summaries, billing amounts, and plan information in one response.
Meter Update (Current Time)POST /billing/metering/{tenant_id}/{unit}Applies immediate metering updates using add / sub / direct method.
Meter Update (Custom Timestamp)POST /billing/metering/{tenant_id}/{unit}/{ts}Updates metering values using a specified timestamp. Useful for corrections.
info

Plan history can be obtained from the plan_histories array in the response of Get Tenant Details in the Auth API.

Plan Period Candidates Endpoint​

Implementing a Period Select Box​

The backend splits plan periods into monthly or yearly segments and returns labels and Unix timestamps (seconds) for the frontend.

info

Use the plan_histories array (which contains plan_id and plan_applied_at) from the response of Get Tenant Details in the Auth API to determine the plan application timing. Treat current_plan_period_end as the final boundary. Exclude entries with an empty plan_id.

info

The following implementation flow and code samples assume a Go backend and a React frontend.

Implementation Flow

  1. Get Tenant Info — Retrieve planHistories and currentPlanPeriodEnd from the Auth API.
  2. Create Boundary Edges — Sort planAppliedAt chronologically to form edge array.
  3. Define Final Boundary — Use currentPlanPeriodEnd (or now if unavailable) minus 1 second as the final edge.
  4. Split Periods — Determine if the interval is monthly or yearly, then generate PlanPeriodOption using step() loop.
  5. Sort by Latest First — Sort by Start in descending order and return to frontend.
// Step 2 & 4: Create edges and split by month/year
type edge struct { PlanID string; Time time.Time }
// ...populate edge array sorted by Time ascending...
for cur := periodStart; !cur.After(periodEnd); {
next := step(cur) // +1M for month, +1Y for year
end := next.Add(-1 * time.Second)
results = append(results, PlanPeriodOption{ Label: label(cur,end), PlanID: e.PlanID, Start: cur.Unix(), End: end.Unix() })
if end.Equal(periodEnd) { break }
cur = end.Add(time.Second)
}

On the frontend, bind these to a <select> element and refetch the dashboard on change:

// BillingDashboard.tsx (excerpt)
const fetchPeriodOptions = async () => {
const res = await axios.get<PlanPeriodOption[]>("/billing/plan_periods", { params:{ tenant_id } });
setPeriodOptions(res.data);
setSelectedPeriod(toState(res.data[0]));
};

Example Implementations​

The following links point to repositories that include implementations of this endpoint.
Search by function name to locate the relevant code.

Dashboard Aggregation Endpoint​

Difference Between max and sum in aggregate_usage​

ModeTypical Use CaseAggregation Method
sum (default)API call counts / trafficUse total value in period
maxConcurrent users / sessionsUse maximum value in period

In calculateMeteringUnitBillings, the function checks aggregate_usage and uses the max or sum accordingly:

if aggUsage == "max" {
for _, c := range resp.JSON200.Counts {
if float64(c.Count) > count { count = float64(c.Count) }
}
} else { // sum
for _, c := range resp.JSON200.Counts { count += float64(c.Count) }
}
info

The aggregate_usage setting is only visible in the UI when Stripe integration is enabled.
It is hidden when using the platform standalone.

Billing Logic by unit_type​

Metering units have a type that determines the billing logic as follows:

typeMeasurement UnitBilling Logic
fixedFixed UnitUse fixed price as is
usageUsage Unitcount × price
tieredTiered UnitAdd prices based on matching tier
tiered_usageTiered Usage UnitAccumulate from lower tiers upward
func calculateAmountByUnitType(count float64, u map[string]interface{}) float64 {
unitType, _ := u["type"].(string)
price, _ := u["unit_amount"].(float64)

switch unitType {
case "fixed":
return price
case "usage":
return count * price
case "tiered":
return calcTiered(count, u)
case "tiered_usage":
return calcTieredUsage(count, u)
default:
// Fallback: treat unknown type as usage
return count * price
}
}

See calcTiered and calcTieredUsage for details.

Example Implementations​

The following links point to repositories that include implementations of this endpoint.
Search by function name to locate the relevant code.

Meter Updates​

The dashboard allows metering updates in two scenarios: inline edits (current time) and modal edits (specific timestamp).

Inline Edit Example (Current Time)​

func updateCountOfNow(c echo.Context) error {
tenantId := c.Param("tenantId")
unitName := c.Param("unit")

userInfo, _ := c.Get(string(ctxlib.UserInfoKey)).(*authapi.UserInfo)
if !hasBillingAccess(userInfo, tenantId) {
return c.String(http.StatusForbidden, "Insufficient permissions")
}

var body struct {
Method string `json:"method"` // add | sub | direct
Count int `json:"count"`
}
if err := c.Bind(&body); err != nil {
return c.String(http.StatusBadRequest, "invalid JSON body")
}
if body.Count < 0 {
return c.String(http.StatusBadRequest, "count must be >= 0")
}

method := pricingapi.UpdateMeteringUnitTimestampCountMethod(body.Method)
switch method {
case pricingapi.Add, pricingapi.Sub, pricingapi.Direct:
// OK
default:
return c.String(http.StatusBadRequest, "method must be add, sub, or direct")
}

param := pricingapi.UpdateMeteringUnitTimestampCountNowParam{
Method: method,
Count: body.Count,
}

resp, err := pricingClient.UpdateMeteringUnitTimestampCountNowWithResponse(
c.Request().Context(), tenantId, unitName, param,
)
if err != nil {
log.Printf("pricing API error: %v", err)
return c.String(http.StatusInternalServerError, "pricing API error")
}
if resp.JSON200 == nil {
return c.String(resp.StatusCode(), string(resp.Body))
}

return c.JSON(http.StatusOK, resp.JSON200)
}
info

The inline plus/minus buttons are only displayed when the selected period is currently ongoing.

Example Implementations​

The following links point to repositories that include implementations of this endpoint.
Search by function name to locate the relevant code.

func updateCountOfSpecifiedTS(c echo.Context) error {
tenantId := c.Param("tenantId")
unitName := c.Param("unit")
tsStr := c.Param("ts")

userInfo, _ := c.Get(string(ctxlib.UserInfoKey)).(*authapi.UserInfo)
if !hasBillingAccess(userInfo, tenantId) {
return c.String(http.StatusForbidden, "Insufficient permissions")
}

ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil {
return c.String(http.StatusBadRequest, "ts must be 10-digit unix seconds")
}

var body struct {
Method string `json:"method"` // add | sub | direct
Count int `json:"count"`
}
if err := c.Bind(&body); err != nil {
return c.String(http.StatusBadRequest, "invalid JSON body")
}
if body.Count < 0 {
return c.String(http.StatusBadRequest, "count must be >= 0")
}

method := pricingapi.UpdateMeteringUnitTimestampCountMethod(body.Method)
switch method {
case pricingapi.Add, pricingapi.Sub, pricingapi.Direct:
// OK
default:
return c.String(http.StatusBadRequest, "method must be add, sub, or direct")
}

param := pricingapi.UpdateMeteringUnitTimestampCountParam{
Method: method,
Count: body.Count,
}

resp, err := pricingClient.UpdateMeteringUnitTimestampCountWithResponse(
c.Request().Context(), tenantId, unitName, int(ts), param,
)
if err != nil {
log.Printf("pricing API error: %v", err)
return c.String(http.StatusInternalServerError, "pricing API error")
}
if resp.JSON200 == nil {
return c.String(resp.StatusCode(), string(resp.Body))
}

return c.JSON(http.StatusOK, resp.JSON200)
}

Example Implementations​

The following links point to repositories that include implementations of this endpoint.
Search by function name to locate the relevant code.