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
| Name | Type | Description | |
|---|---|---|---|
userId | required | string | Your end-user's ID. We do not authenticate them - pass whatever string you use internally. |
event | required | string | Event type. Conventional dot-notation, e.g. image.render. |
quantity | optional | number | How much was consumed. Defaults to 1. Must be positive. |
metadata | optional | Record<string, string> | Flat key/value pairs. Used by limit-group match rules and shown in analytics. |
options.prompt | optional | string | The 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.response | optional | string | The 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'] })
}[];
}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 },
);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 totrackwith apk_live_public key.invalid_request(400) - missing/invalid field.
When to use track vs reserve
Use
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).