vevee.track()POST /api/v1/tracksk_live_

Records that an end-user consumed something. Increments every matching limit group and returns the new counter summaries. Throws if the user is already over a limit.

Signature

track(
  userId: string,
  event: string,
  quantity?: number,             // default: 1
  metadata?: EventMetadata,      // Record<string, string>
  options?: {
    prompt?: string;             // input sent to your AI model
    response?: string;           // model's output
  },
): Promise<TrackResponseData>

Parameters

NameTypeDescription
userIdrequiredstringYour end-user's ID. We do not authenticate them - pass whatever string you use internally.
eventrequiredstringEvent type. Conventional dot-notation, e.g. image.render.
quantityoptionalnumberHow much was consumed. Defaults to 1. Must be positive.
metadataoptionalRecord<string, string>Flat key/value pairs. Used by limit-group match rules and shown in analytics.
options.promptoptionalstringThe text prompt your user sent to the AI model. Persisted in event_logs for debugging and product analytics. Capped at 32 KB server-side; longer values are truncated. Silently ignored if prompt logging is disabled for the app (Settings → Prompt logging).
options.responseoptionalstringThe model's response. Same 32 KB cap and silent-ignore behavior as prompt.

Response

interface TrackResponseData {
  eventId: string;                  // 'evt_…'
  matchStatus:                      // shown as a status badge in the dashboard
    | 'matched'                     //   → counted against at least one limit group
    | 'unmatched'                   //   → recorded but no limit group covered it
    | 'blocked'                     //   → matched, but a quota was already at zero
    | 'no_subscription';            //   → user has no active subscription on this app
  matchedGroupIds: string[];        // 'lg_…' for each group counted; empty otherwise
  counters: {
    groupId: string;                // 'lg_…'
    label: string;                  // group label from the dashboard
    unit: 'count' | 'tokens' | 'seconds' | 'cents';
    quota: number;                  // limit on the user's plan
    count: number;                  // value AFTER incrementing
    remaining: number;              // max(0, quota - count) - clamped, never negative
    costCents: number;              // accumulated cost in cents
    filters: Record<string, string[]>; // metadata gates (e.g. { source: ['text'] })
  }[];
}
i
Every event is recorded- even when no limit group matches or the user has no subscription. They show up in the dashboard's Eventstab with a status badge (Counted / Not matched / Limit reached / No plan), and unmatched rows expose a “did you mean?” suggestion to surface typos. Use canUse / reserve for fail-closed enforcement (they return matched: false for the same situations).

Example

import { createClient, VeveeError } from '@vevee/sdk';

const vevee = createClient({ apiKey: process.env.VEVEE_KEY! });

try {
  const result = await vevee.track('user_abc123', 'llm.completion', 1842, {
    model: 'gpt-4o',
    inputTokens: '920',
    outputTokens: '922',
  });

  console.log(result.eventId);          // 'evt_8sk2…'
  console.log(result.counters);
  // [{ groupId: 'lg_tokens_monthly', count: 184213, costCents: 553 }]
} catch (err) {
  if (err instanceof VeveeError && err.code === 'limit_reached') {
    return res.status(429).json({ error: 'Out of tokens this month' });
  }
  throw err;
}

With prompt logging

Pass the prompt and response to the optional options object to capture them alongside the event. Enable prompt logging first in Settings → Prompt loggingon the app - when the toggle is off, these fields are silently ignored, so it's safe to leave them in your code.

const prompt = 'Write a haiku about Postgres';
const completion = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: prompt }],
});
const response = completion.choices[0]?.message.content ?? '';

await vevee.track('user_abc123', 'llm.completion', completion.usage?.total_tokens ?? 0,
  { model: 'gpt-4o' },
  { prompt, response },
);
i
Prompts are not searched on hot paths. They live in a sibling event_logstable that's only read when you open an event in the dashboard, so charts and quotas stay fast. See Prompt logging for retention and privacy details.

Errors

  • limit_reached (429) - at least one matching limit group is at quota.
  • workspace_limit_reached (429) - your Vevee workspace hit its event quota.
  • invalid_key (401) - bad or revoked API key.
  • requires_secret_key (403) - you tried to track with a pk_live_ public key.
  • invalid_request (400) - missing/invalid field.

When to use track vs reserve

i
Use track() for cheap, idempotent events you can absorb a tiny over-count on (analytics-style, page.viewed, chat.message).
Use reserve() for anything that costs real money or where parallel requests must not exceed quota (image gen, video gen, expensive LLM calls).