Skip to content

Sell prepaid credits

Say customers buy credits in packs, and every member of a customer’s team spends from the same balance. That’s a balance limit on the team, with grants when they buy.

The window total never resets. The amount is what every team starts with, here 100 free credits:

import { createClient } from "@limitry/sdk";
const limitry = createClient();
await limitry.limits.create({
name: "Credits",
subjectKind: "team",
action: "*",
amount: 100,
unit: "credits",
window: "total",
});

In your payment webhook, grant what they bought. Use the payment’s id as the idempotency key, so a redelivered webhook can’t add credits twice:

import { createClient } from "@limitry/sdk";
const limitry = createClient();
await limitry.limits.grants.create(
"lim_3f0c…",
{
subject: { kind: "team", id: "team_42" },
amount: 5000,
note: "5,000-credit pack",
},
{ idempotencyKey: "payment_8231" },
);

Credits go to the balance in the key’s environment. Your payment handler in production needs a live key (lim_live_…), with $LIMIT_ID the live limit’s id. A test key adds test credits, which suits your payment provider’s test mode. See Test and live.

Check as the user, and name the team as the shared subject. Any limits you set on the user still apply as well:

const report = await limitry.guard(
{
subject: { kind: "user", id: user.id },
shared: [{ kind: "team", id: user.teamId }],
action: "report",
cost: 25,
},
() => buildReport(params),
);

However many teammates spend at the same moment, a check never takes the balance below zero: Limitry refuses a cost that doesn’t fit. Two things can still take it below zero: committing a reservation at more than it held, which Limitry charges in full (see Reservations), and taking back credits the team already spent (below).

The check says no, with retryAfterSeconds: null, because waiting won’t help. Use that to offer a top-up rather than “try again later”.

To show the balance in your product, read remaining from any check’s answer, or make a preview check, which counts nothing.

In your refund webhook, take the credits back with a negative amount, a note, and the refund’s id as the idempotency key:

import { createClient } from "@limitry/sdk";
const limitry = createClient();
await limitry.limits.grants.create(
"lim_3f0c…",
{
subject: { kind: "team", id: "team_42" },
amount: -5000,
note: "Refund of payment_8231",
},
{ idempotencyKey: "refund_311" },
);

If the team already spent the credits, the balance goes below zero and their checks say no until they buy more. A chargeback works the same way.

Related: Pools, balances and budgets, Idempotency.