Returns the user's current counter values for the active period. This is the only endpoint that accepts a pk_live_ public key - making it safe to call from browser, mobile, or any client-side code.
Signature
usage(userId: string, event?: string): Promise<UsageResponseData>Parameters
| Name | Type | Description | |
|---|---|---|---|
userId | required | string | End-user ID. With a pk_live_ key, this must match the caller. |
event | optional | string | Narrow the response to limit groups whose match rules cover this event. Accepts a concrete event name ("image.gemini-3-1-flash-image-preview") or a glob ("image.*", "feature.*","*"). Omit to get every counter on the user's plan. |
Response
interface UsageResponseData {
userId: string; // the id you passed
canonicalId?: string; // secret key only: the id usage is stored under (differs when userId is an alias)
// The active subscription, or null when there is none (SDK 1.12.0+).
subscription: {
planId: string;
endsAt: string | null; // set when a cancel is scheduled
} | null;
// null exactly when subscription is null.
period: {
start: string; // ISO 8601
end: string | null; // null for lifetime plans
} | null;
counters: {
groupId: string;
label: string; // human label from the dashboard
unit: 'count' | 'tokens' | 'seconds' | 'cents';
quota: number; // total allowed in this period
count: number; // already used
remaining: number; // max(0, quota - count) - pre-computed, never negative
costCents: number;
// Distinct metadata filter values from the group's match rules.
// {} when the group has no metadata gate ("overall" buckets);
// e.g. { source: ['text'] } or { variant: ['4k'] } when split.
filters: Record<string, string[]>;
}[];
}count: 0). Use filters to tell “overall” buckets apart from per-source / per-variant splits when a plan layers multiple groups over the same model.Examples
From a backend (full visibility)
const usage = await vevee.usage('user_abc123');
console.log(usage.period);
// { start: '2026-05-01T00:00:00Z', end: '2026-06-01T00:00:00Z' }
for (const c of usage.counters) {
// 'Images (text source) - 1/2 used, 1 left ($0.04)'
const tag = Object.entries(c.filters)
.map(([k, vs]) => `${k}=${vs.join('|')}`)
.join(', ');
console.log(
`${c.label}${tag ? ` [${tag}]` : ''} - ${c.count}/${c.quota} used, ` +
`${c.remaining} left ($${(c.costCents / 100).toFixed(2)})`,
);
}From the browser (public key)
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@vevee/sdk';
const vevee = createClient({ apiKey: process.env.NEXT_PUBLIC_VEVEE_KEY! });
export function UsageBadge({ userId }: { userId: string }) {
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
vevee.usage(userId).then((u) => {
// every group on the user's plan is in the response - even ones at 0
const total = u.counters.find((c) => c.label === 'Images');
setCount(total?.remaining ?? 0);
});
}, [userId]);
return <span>{count ?? '…'} images left this month</span>;
}Filtering by event (concrete or wildcard)
Define your event names as constants alongside your track() calls, then reuse them on usage() to ask for only the counters that move when that event fires. Globs let you query whole event families at once - useful when one feature emits several model-specific event types.
export const VEVEE_EVENTS = {
GEMINI_3_1: 'image.gemini-3-1-flash-image-preview',
GEMINI_2_5: 'image.gemini-2-5-flash-image',
FEATURE_SCAN: 'feature.scan',
FEATURE_LINK: 'feature.link',
} as const;
// Counters for groups whose match rules cover this exact event.
const gemini = await vevee.usage(userId, VEVEE_EVENTS.GEMINI_3_1);
// Counters for any group reached by an "image.*" event.
const allImages = await vevee.usage(userId, 'image.*');
// Every counter on the user's plan.
const everything = await vevee.usage(userId);matches in your plan). A group is included whenever your query and any of its rules could share even one concrete event name - so image.* picks up groups whose rules areimage.gemini-3-1-flash-image-preview, image.*, or *.pk_live_ key, the API enforces that userIdmatches the requesting user's ID (passed via your auth integration). Other users' usage is never returned.No active subscription
A user without an active subscription gets one of two answers. With no usable credit balance, usage() throws subscription_not_found (below). With a usable credit balance it resolves normally with subscription: null, period: null, empty counters and the balances in credits, so the user can still spend credits. Check subscription === null rather than inferring it from period.
try {
const usage = await vevee.usage(userId);
if (usage.subscription === null) showUpsell(usage.credits); // credits only
} catch (e) {
if (e instanceof VeveeError && e.code === 'subscription_not_found') showUpsell([]);
else throw e;
}Errors
invalid_key(401)subscription_not_found(404) - no active subscription exists for this user and no usable credit balance. CallupsertSubscription()first when the user signs up.