Types

All public types are re-exported from @vevee/sdk. They are the single source of truth for the wire format of every endpoint.

Client

import type { ClientOptions, AnalyticsClientConfig, AnalyticsMode } from '@vevee/sdk';

interface ClientOptions {
  apiKey: string;                    // 'sk_live_…' or 'pk_live_…'
  baseUrl?: string;                  // default: 'https://www.vevee.org'
  analytics?: AnalyticsClientConfig;
}

interface AnalyticsClientConfig {
  mode?: AnalyticsMode;              // default: 'hybrid'
  requireConsent?: boolean;          // default: true
}

type AnalyticsMode = 'hybrid' | 'identified' | 'aggregate';

Metering events

type EventMetadata = Record<string, string>;

interface TrackResponseData {
  eventId: string;
  counters: CounterSummary[];
}

interface CounterSummary {
  groupId: string;
  event: string;                         // raw event_type pattern (e.g. "image.gemini-3-1-flash-image-preview" or "image.*")
  label: string;                         // human label from the dashboard
  unit: 'count' | 'tokens' | 'seconds' | 'cents';
  quota: number;                         // limit set on the plan
  count: number;                         // current usage in this period
  remaining: number;                     // max(0, quota - count) - pre-computed
  costCents: number;
  // Distinct metadata filter values aggregated across this group's match
  // rules. {} for "overall" groups; e.g. { source: ['text'] } when the
  // group is gated to a specific source / variant.
  filters: Record<string, string[]>;
}

canUse

interface CanUseResponseData {
  allowed: boolean;
  matched: boolean;
  reasons: string[];
  details: {
    groupId: string;
    current: number;
    quota: number;
    resetsAt: string | null;         // ISO 8601 or null for lifetime
    creditAvailable: number;
    creditPacks: { packId: string; packName: string; available: number }[];
  }[];
}

Reservations

interface ReserveResponseData {
  allowed: boolean;
  matched: boolean;
  reservationId?: string;
  expiresAt?: string;
  reasons?: string[];
}

Usage

interface UsageResponseData {
  userId: string;
  period: { start: string; end: string | null } | null;
  counters: CounterSummary[];
  credits: CreditBalanceSummary[];
}

Subscriptions

interface UpsertSubscriptionRequest {
  userId: string;
  planId: string;
  customLimits?: PlanLimits;
  endsAt?: string;
  // Align relative-period counters with an external billing provider.
  // Omit to preserve any previously-set anchor; pass null to clear it.
  cycleStart?: string | null;
}

interface UpsertSubscriptionResponseData {
  subscriptionId: string;
  userId: string;
  planId: string;
  startedAt: string;
  cycleAnchorAt: string | null;
}

Plan structure

type PeriodType = 'daily' | 'weekly' | 'monthly' | 'lifetime';
type PeriodAnchor = 'subscription_start' | 'calendar';
type LimitUnit = 'count' | 'tokens' | 'seconds' | 'cents';

interface MatchRule {
  event: string;                     // exact ('image.flux-pro') or glob ('image.*')
  metadata?: Record<string, string>; // every key/value must match (values support globs)
}

interface LimitGroup {
  id: string;
  label: string;
  unit: LimitUnit;
  quota: number;
  matches: MatchRule[];              // event matches the group if ANY rule matches
}

interface PlanLimits {
  groups: LimitGroup[];
}

Analytics - capture / identify / alias

type AnalyticsPropertyValue = string | number | boolean | null;
type PersonProfile = Record<string, AnalyticsPropertyValue>;

interface AnalyticsProperties {
  $set?: PersonProfile;        // overwrite the person profile
  $set_once?: PersonProfile;   // first-write-wins on the person profile
  [key: string]: AnalyticsPropertyValue | PersonProfile | undefined;
}

// POST /api/v1/capture
interface AnalyticsCaptureRequest {
  // Omit for anonymous aggregate (hybrid mode, pre-login). Pass a real user
  // id post-login. Never pass a generated anonymous UUID - that needs browser
  // storage and a cookie banner.
  distinctId?: string;
  event: string;
  properties?: AnalyticsProperties;
  timestamp?: string;
}

interface AnalyticsCaptureResponseData {
  eventId: string;            // 'anv_…' anonymous · 'aev_…' identified
  isAnonymous: boolean;       // true → routed to analytics_anonymous_events
  personId?: string;          // identified events only
  isReserved?: boolean;       // identified events only
}

// POST /api/v1/identify
interface IdentifyOptions {
  mergeAnonymousId?: string;  // REQUIRES consentGiven: true
  consentGiven?: boolean;
}

interface AnalyticsIdentifyRequest {
  distinctId: string;
  properties?: PersonProfile;      // $set
  propertiesOnce?: PersonProfile;  // $set_once
  mergeAnonymousId?: string;
  consentGiven?: boolean;
}

interface AnalyticsIdentifyResponseData {
  personId: string;
  merged: boolean;
}

// POST /api/v1/alias
interface AliasOptions {
  consentGiven?: boolean;     // required true when either id is anonymous (anon_ prefix)
}

interface AnalyticsAliasRequest {
  distinctId: string;
  alias: string;
  consentGiven?: boolean;
}

interface AnalyticsAliasResponseData {
  personId: string;
}

Analytics - privacy / GDPR (secret key only)

// POST /api/v1/opt-out · POST /api/v1/opt-in
interface AnalyticsOptRequest {
  distinctId: string;
}

// GET /api/v1/opted-out?distinctId=…
interface AnalyticsOptedOutResponseData {
  optedOut: boolean;
}

// POST /api/v1/delete-person  (GDPR Art. 17 - right to erasure)
interface AnalyticsDeletePersonRequest {
  distinctId: string;
}
interface AnalyticsDeletePersonResponseData {
  jobId: string;
}

// GET /api/v1/deletion-status?jobId=…
type DeletionJobStatus = 'queued' | 'in_progress' | 'completed' | 'failed';
interface AnalyticsDeletionStatusResponseData {
  status: DeletionJobStatus;
  startedAt?: string;
  completedAt?: string;
  errorMessage?: string;
}

// POST /api/v1/export-person  (GDPR Art. 15 / 20 - access & portability)
interface AnalyticsExportPersonRequest {
  distinctId: string;
}
interface AnalyticsExportPersonResponseData {
  downloadUrl: string;
  expiresAt: string;       // ISO 8601 - URL valid 24h
  format: 'json';
}

Reserved-event taxonomy

interface ReservedEventSpec {
  name: string;
  category: 'identity' | 'onboarding' | 'paywall' | 'checkout'
          | 'subscription' | 'feature' | 'trial';
  description: string;
  conventionalProperties: { key: string; description: string; example?: string }[];
}

declare const RESERVED_EVENTS: readonly ReservedEventSpec[];
type ReservedEventName = (typeof RESERVED_EVENTS)[number]['name'];

declare function isReservedEvent(name: string): name is ReservedEventName;

Error envelope

type ErrorCode =
  | 'not_found'
  | 'invalid_key'
  | 'requires_secret_key'
  | 'limit_reached'
  | 'unmatched_event'
  | 'no_subscription'
  | 'workspace_limit_reached'
  | 'invalid_request'
  | 'reservation_expired'
  | 'reservation_not_pending'
  | 'already_canceled'
  | 'internal_error'
  | 'not_implemented'
  // Thrown when a consent-gated analytics operation (anonymous→identified
  // merge) is attempted without consentGiven: true.
  | 'consent_required';

interface ApiError { code: ErrorCode; message: string; }
type ApiResponse<T> = { ok: true; data: T } | { ok: false; error: ApiError };

ID prefixes

PrefixResource
ws_Workspace
app_App
plan_Plan
lg_Limit group
sub_Subscription
evt_Metering event
cnt_Counter
rsv_Reservation
aev_Identified analytics event
anv_Anonymous analytics event
pid_Analytics person
cau_Consent-audit entry
del_Deletion job
anon_Anonymous-session id (from getAnonymousId())