Returns the list of plans you've defined for this app - exactly as configured in the dashboard. Use it to render a pricing page from a single source of truth instead of duplicating plan structure in your frontend code.
Signature
availablePlans(): Promise<PublicPlan[]>Safe to call from the browser with a pk_* key. No parameters - the API key already scopes the response to your app.
Response
interface PublicPlan {
id: string; // 'plan_…' - pass to upsertSubscription()
name: string; // 'Free', 'Pro', etc. (display label)
period: {
type: 'daily' | 'weekly' | 'monthly' | 'lifetime';
anchor: 'subscription_start' | 'calendar';
};
limits: PublicLimitGroup[];
}
interface PublicLimitGroup {
id: string;
label: string; // user-facing, e.g. 'Image generations'
unit: 'count' | 'tokens' | 'seconds' | 'cents';
quota: number;
event: string; // first event_type pattern (e.g. 'image.*')
matches: MatchRule[]; // full match rules incl. metadata filters
}
interface MatchRule {
event: string; // 'image.gemini-3.1-image' or 'image.*'
metadata?: Record<string, string>;
}Plans are returned in the order they were created (oldest first). If you need a specific display order on your pricing page, sort client-side by name or by a hand-maintained array of plan ids.
Live vs test mode
Plans are shared between live and test mode - a pk_test_ key returns the same list as pk_live_. This is intentional: it lets you preview your real pricing page from a sandboxed end-user account without maintaining a duplicate plan catalogue.
Examples
Render a pricing page (React)
'use client';
import { useEffect, useState } from 'react';
import { createClient, type PublicPlan } from '@vevee/sdk';
const vevee = createClient({ apiKey: process.env.NEXT_PUBLIC_VEVEE_KEY! });
export function PricingTable() {
const [plans, setPlans] = useState<PublicPlan[]>([]);
useEffect(() => {
vevee.availablePlans().then(setPlans);
}, []);
return (
<div className="pricing-grid">
{plans.map((plan) => (
<div key={plan.id} className="pricing-card">
<h3>{plan.name}</h3>
<p>Resets {plan.period.type}</p>
<ul>
{plan.limits.map((g) => (
<li key={g.id}>
<strong>{g.quota.toLocaleString()}</strong> {g.label}
{g.matches.some((m) => m.metadata) && (
<small> (only {describeFilters(g.matches)})</small>
)}
</li>
))}
</ul>
<button onClick={() => subscribe(plan.id)}>Choose {plan.name}</button>
</div>
))}
</div>
);
}Map matches to user-facing labels
The raw event pattern is great for engineers, but on a pricing page you usually want human-readable copy. Keep a small mapping table next to your UI:
const EVENT_COPY: Record<string, string> = {
'image.*': 'Image generations',
'image.flux-pro': 'Flux Pro images',
'video.*': 'Video seconds',
'llm.tokens': 'LLM tokens',
};
function pricingLabel(g: PublicLimitGroup): string {
// Prefer the label set in the dashboard; fall back to event-pattern copy.
return g.label || EVENT_COPY[g.event] || g.event;
}Server-side rendering (Next.js)
// app/pricing/page.tsx - runs on the server, no JS shipped for the data fetch.
import { createClient } from '@vevee/sdk';
export const revalidate = 300; // cache 5 min - plans change rarely.
export default async function PricingPage() {
const vevee = createClient({ apiKey: process.env.VEVEE_PUBLIC_KEY! });
const plans = await vevee.availablePlans();
return (
<main>
{plans.map((plan) => (
<article key={plan.id}>
<h2>{plan.name}</h2>
{plan.limits.map((g) => (
<p key={g.id}>{g.quota.toLocaleString()} {g.label}</p>
))}
</article>
))}
</main>
);
}What's exposed (and what isn't)
- Exposed: plan id, name, period, limit-group label / unit / quota / event pattern / match rules including metadata filters.
- Not exposed:
onPlanChangebehaviour (internal counter policy) and any pricing-rule cost data. The endpoint returns only what a pricing page needs to render. - Prices: not stored on plans today - keep your
$/mocopy alongside yourplanId → display copymapping in your frontend, or store it in your CMS.
After a user picks a plan
Pass the returned plan.id straight to upsertSubscription(). You typically run that on your backend after a successful Stripe checkout - the same plan id you read from the browser also flows through your Stripe metadata.
Errors
invalid_key(401) - missing, malformed, or revoked API key.
See also
- Guide: show users your plan features
- upsertSubscription()
- usage() - read a user's consumption against their current plan.