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

NameTypeDescription
userIdrequiredstringEnd-user ID. With a pk_live_ key, this must match the caller.
eventoptionalstringNarrow 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[]>;
  }[];
}
i
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);
i
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.
i
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.

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. Call upsertSubscription() first when the user signs up.