vevee.usage()GET /api/v1/usagepk_live_ or sk_live_
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;
period: {
start: string; // ISO 8601
end: string | null; // null for lifetime plans
};
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[]>;
}[];
}The response includes every limit group on the user's plan, including ones with no events yet (
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);Matching is done against each limit group's configured rule patterns (the
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 *.Walking through a real plan with overall + per-source splits? See Reading usage and remaining quota for an end-to-end example.
When called with a
pk_live_ key, the API enforces that userIdmatches the requesting user's ID (passed via your auth integration). Other users' usage is never returned.Errors
invalid_key(401)subscription_not_found(404) - no active subscription exists for this user. CallupsertSubscription()first when the user signs up.