Show users where they stand
Your users want to know how much they have left before they run out: “4,975 credits left for your team”. Limitry knows the exact numbers for every limit that applies to a user, including the balances they share with their team. This guide uses the team credits from Sell prepaid credits. This guide adds a route on your server that asks Limitry, and a React component that shows the answer.
Limitry doesn’t host the screen. You copy the component, style it, and it stays yours.
1. Ask Limitry from your server
Section titled “1. Ask Limitry from your server”limitry.standing() returns every enabled limit on one subject, with
what’s used, what’s left and when it resets. Name shared subjects too,
such as the user’s team, and their limits come back in the same list:
import { createClient } from "@limitry/sdk";
const limitry = createClient(); // reads LIMITRY_API_KEY
// Express-style handler for the signed-in user.app.get("/api/usage", async (req, res) => { const user = req.user; const standing = await limitry.standing({ subject: { kind: "user", id: user.id }, shared: [{ kind: "team", id: user.teamId }], }); res.json(standing);});from flask import Flask, jsonifyfrom limitry import Limitry
app = Flask(__name__)limitry = Limitry() # reads LIMITRY_API_KEY
# Flask handler for the signed-in user.@app.get("/api/usage")def usage(): user = current_user() standing = limitry.standing( subject={"kind": "user", "id": user.id}, shared=[{"kind": "team", "id": user.team_id}], ) return jsonify(standing)Call it from your server with your API key, and only for the signed-in
user. The key can read every subject in the workspace, so it must never
reach a browser. The key needs Limits: View (limits:read), which
Check limits includes.
Asking isn’t a check. Limitry counts nothing, and it doesn’t use your plan’s checks.
The same question over HTTP:
import { createClient } from "@limitry/sdk";
const limitry = createClient();
const standing = await limitry.usage.standing({ kind: "user", id: "usr_123", shared: "team:team_42",});from limitry import Limitry
limitry = Limitry()
standing = limitry.usage.standing(kind="user", id="usr_123", shared="team:team_42")limitry usage standing --kind user --id usr_123 --shared team:team_42curl "https://api.limitry.com/v1/usage/standing?kind=user&id=usr_123&shared=team:team_42" \ -H "Authorization: Bearer $LIMITRY_API_KEY"shared takes up to 4 subjects as kind:id pairs separated by commas.
2. What comes back
Section titled “2. What comes back”{ "subject": { "kind": "user", "id": "usr_123" }, "environment": "live", "limits": [ { "limit": "lim_…", "name": "Credits", "subject": { "kind": "team", "id": "team_42" }, "shared": true, "action": "*", "unit": "credits", "window": "total", "rolling": false, "amount": 100, "used": 125, "remaining": 4975, "resetsAt": null } ]}Each item is one limit. Here it’s the team’s balance: 100 free credits
to start, a 5,000-credit pack bought since, and 125 spent. shared is
true because the limit is on a shared subject (the team) rather than
on the user. A few cases to handle:
- No limits: a user with no limit in this environment gets an empty
limitslist. Show nothing. - Balances (window
total):resetsAtisnull, because a balance never resets. After you grant credits,remainingcan be more thanamount, as above, so show “credits left” rather than a share ofamount. - Limits that reset (
day,monthand the rest): show “18 of 50 left” and when it resets. - Rolling limits:
resetsAtis when the oldest use in the window leaves it, which is when the next unit frees up. - Concurrent limits:
usedis how many are running now.
The numbers are exact at the moment you ask, the same counts a check sees. A test key reads test usage and a live key reads live usage.
3. The usage meter
Section titled “3. The usage meter”Copy this component into your app. It needs React and nothing else, and it reads the route from step 1:
import { useEffect, useState } from "react";
type Limit = { limit: string; name: string; unit: string; window: string; amount: number; used: number; remaining: number; resetsAt: string | null;};
export function UsageMeter() { const [limits, setLimits] = useState<Limit[] | null>(null);
useEffect(() => { fetch("/api/usage") .then((res) => (res.ok ? res.json() : { limits: [] })) .then((body) => setLimits(body.limits)) .catch(() => setLimits([])); }, []);
if (!limits || limits.length === 0) return null;
return ( <ul style={{ listStyle: "none", padding: 0 }}> {limits.map((l) => ( <li key={l.limit} style={{ marginBottom: 16 }}> <UsageRow limit={l} /> </li> ))} </ul> );}
function UsageRow({ limit }: { limit: Limit }) { const balance = limit.window === "total"; const max = Math.max(limit.amount, limit.used + limit.remaining); const left = Math.max(limit.remaining, 0); const label = balance ? `${left} ${limit.unit} left` : `${left} of ${limit.amount} ${limit.unit} left`; const resets = limit.resetsAt ? `Resets ${new Date(limit.resetsAt).toLocaleString()}` : null;
return ( <div> <div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}> <span>{limit.name}</span> <span>{label}</span> </div> <div role="meter" aria-label={limit.name} aria-valuemin={0} aria-valuemax={max} aria-valuenow={left} aria-valuetext={label} style={{ height: 8, borderRadius: 4, background: "#e5e7eb", overflow: "hidden", }} > <div style={{ width: `${max > 0 ? (left / max) * 100 : 0}%`, height: "100%", background: left === 0 ? "#b91c1c" : "#1d4ed8", }} /> </div> {resets && <small>{resets}</small>} </div> );}The bar shows what’s left, so it empties as the team spends. Screen readers announce the same words the user sees, such as “4975 credits left”. Swap the inline styles for your own classes.
Fetch it when the page opens, not on a timer. If your product shows the meter on every page, cache the route’s answer for a few seconds on your server.
4. When they run out
Section titled “4. When they run out”A check that’s turned away throws LimitExceeded from limitry.guard()
and limitry.hold(). Answer it where your product talks to the user, and
point them at the meter or a way to get more:
import { LimitExceeded } from "@limitry/sdk";
try { return await limitry.guard( { subject: { kind: "user", id: user.id }, shared: [{ kind: "team", id: user.teamId }], action: "report", cost: 25, }, () => buildReport(params), );} catch (err) { if (err instanceof LimitExceeded && err.reason === "limit_exceeded") { return res.status(402).json({ message: "Your team is out of credits. Buy more to keep going.", buyUrl: "/billing/credits", }); } throw err;}from flask import jsonifyfrom limitry import LimitExceeded
try: with limitry.guard( subject={"kind": "user", "id": user.id}, shared=[{"kind": "team", "id": user.team_id}], action="report", cost=25, ): return build_report(params)except LimitExceeded as err: if err.reason == "limit_exceeded": return jsonify( message="Your team is out of credits. Buy more to keep going.", buyUrl="/billing/credits", ), 402 raiseA balance’s retryAfterSeconds is null, because waiting won’t help.
Show a “Buy more credits” button when remaining reaches 0, and grant
the credits from your payment webhook (see
Sell prepaid credits). For a limit that
resets, tell the user when, from err.retryAfterSeconds.
To tell users before they run out, see Warn your users before they hit a limit.