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;
  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[]>;
  }[];
}
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.

Errors

  • invalid_key (401)
  • subscription_not_found (404) - no active subscription exists for this user. Call upsertSubscription() first when the user signs up.