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
    // Cost options - mutually exclusive sets:
    costCents?: number;          // single total cost in cents
    inputCostCents?: number;     // input portion; summed with outputCostCents
    outputCostCents?: number;    // output portion; summed with inputCostCents
  },
): 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.

Recording actual cost

Pass explicit cost values in options to write the real cost of an event to the dashboard - useful for custom events (e.g. video.analyze) or for LLM calls where you compute tier-aware pricing yourself. The values appear in the overall app cost chart, the per-user coston the subscriber's detail page, and the per-event breakdown.

OptionTypeDescription
options.costCentsoptionalnumberA single total cost in cents. Use when input and output are not tracked separately. Must be non-negative. Mutually exclusive with inputCostCents / outputCostCents.
options.inputCostCentsoptionalnumberInput portion of the cost in cents. The server sums input and output into the total. Must be non-negative.
options.outputCostCentsoptionalnumberOutput portion of the cost in cents. Summed with inputCostCents. Must be non-negative.
Passing both costCents and inputCostCents / outputCostCents in the same call returns an invalid_request error - they are mutually exclusive. Catalog-priced events (e.g. image.<model>) still use catalog pricing when no explicit cost is supplied; explicit values override the catalog estimate when both are present.
// Split input / output cost (e.g. a video analysis you priced yourself)
await vevee.track(userId, 'video.analyze', durationSeconds, { videoId }, {
  inputCostCents: 30,
  outputCostCents: 12,
});

// Single total cost
await vevee.track(userId, 'api.call', 1, { endpoint: '/summarize' }, {
  costCents: 42,
});

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).