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
| 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) or the end user opted out of 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'] })
}[];
}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 },
);event_logstable that's only read when you open an event in the dashboard, so charts and quotas stay fast. Prompt logging is off by default. To stop it for a single end user, call analytics.optOut(userId, 'prompt_logging'); the event is still metered, only the text is dropped.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.
| Option | Type | Description | |
|---|---|---|---|
options.costCents | optional | number | A single total cost in cents. Use when input and output are not tracked separately. Must be non-negative. Mutually exclusive with inputCostCents / outputCostCents. |
options.inputCostCents | optional | number | Input portion of the cost in cents. The server sums input and output into the total. Must be non-negative. |
options.outputCostCents | optional | number | Output portion of the cost in cents. Summed with inputCostCents. Must be non-negative. |
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 totrackwith apk_live_public key.invalid_request(400) - missing/invalid field.
When to use track vs reserve
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).