Skip to content

Warn your users before they hit a limit

A user who hits a limit without warning feels cut off. A user who got an email at 80% had time to slow down or buy more. Limitry sends your server a webhook when a subject reaches 80% and 100% of a limit, and your server decides what to tell the user.

This guide uses the $2 monthly AI budget from Limit AI spend per user of your product, a limit of 200 cents a month on each user. The warning at 80% arrives when a user has spent 160 cents.

Limitry never emails your users. You send the message, from your own domain, in your own words.

In the app, open Settings → Webhooks, add your endpoint’s URL, pick “Only these” and choose limit.threshold_crossed. Copy the signing secret. It’s shown once. Or with the API, using a live key:

import { createClient } from "@limitry/sdk";
const limitry = createClient({ token: process.env.LIMITRY_LIVE_KEY });
await limitry.workspace.webhookEndpoints.create({
url: "https://example.com/hooks/limitry",
eventTypes: ["limit.threshold_crossed"],
});

The handler verifies the signature, ignores repeats and test traffic, and emails the user:

import { verifyWebhook, WebhookVerificationError } from "@limitry/sdk";
// What a limit.threshold_crossed event carries.
type ThresholdCrossed = {
subject: { kind: string; id: string };
environment: "test" | "live";
threshold: 80 | 100;
used: number;
amount: number;
resetsAt: string | null;
};
// `rawBody` must be the request body exactly as it arrived.
app.post("/hooks/limitry", async (req, res) => {
let event;
try {
event = await verifyWebhook(
req.rawBody,
req.headers,
process.env.LIMITRY_WEBHOOK_SECRET!,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return res.status(400).send("bad signature");
}
throw error;
}
res.status(200).send("ok"); // answer first, work after
if (event.type !== "limit.threshold_crossed") return;
const data = event.data as ThresholdCrossed;
if (data.environment !== "live") return;
if (data.subject.kind !== "user") return;
// A warning resent after an outage keeps its id: send one email per id.
if (!(await markProcessed(event.id))) return;
const user = await db.users.find(data.subject.id);
if (!user) return;
const resets = data.resetsAt
? ` It resets ${new Date(data.resetsAt).toUTCString()}.`
: "";
await sendEmail({
to: user.email,
subject:
data.threshold === 80
? "You've used 80% of your allowance"
: "You've used all of your allowance",
text:
`You've used ${data.used} of ${data.amount}.` +
resets +
" See your usage at https://example.com/usage.",
});
});

markProcessed records the event id and returns false if it was already there, for example an insert into a table with the id as its primary key. sendEmail is your email provider.

For prepaid credits, a limit with window total, threshold: 80 means 20% of the credits are left and threshold: 100 means none are. A balance never resets, so resetsAt is null. Link to your page for buying more instead of a reset time. After you grant credits, the balance can warn again the next time it runs low.

The payload and how often each kind of limit warns are in Webhooks.

The endpoint from step 1 belongs to live and receives only live events. To try the warning, add a test endpoint the same way, with the app on test or with a test key (lim_test_…). It has its own signing secret. Point it at a test copy of your handler, or let your handler through for test events while you try it.

Then, with the test key, make ai-call checks for one user until they pass 80% of the test budget, for example a check with a cost of 170. The warning reaches the test endpoint within about a minute, with "environment": "test".

An email says “you’re close”. A usage meter in your app says exactly how close at any time. See Show users where they stand.