vevee.compose()POST /api/v1/composesk_live_

Runs a compose typeyou defined in the dashboard - its prompt, data sources, model, and output schema all live server-side. Your code just names the type and passes the end-user it's for; Vevee assembles the prompt, calls the model, meters the AI cost against that user, and returns typed, structured output.

Signature

compose<T = unknown>(
  type: string,                  // the compose type's name (as set in the dashboard)
  userId: string,                // your end-user's id
  vars?: Record<string, unknown> // optional template variables
): Promise<ComposeResult<T>>

Backend-only - it spends your AI budget, so it requires a sk_* secret key and will reject a pk_* key with requires_secret_key. The generic Tis the shape of your type's structured output, which you define to match the output schema configured for the type in the dashboard.

Parameters

  • type - the name of the compose type as configured in the dashboard. An unknown name throws not_found.
  • userId - the end-user this generation is for. The AI cost is metered against this user, and any per-user data sources on the type resolve to their data.
  • vars- optional key/value pairs injected into the type's prompt template. Use these for the bits that change per call (a topic, a tone, a product name).

Response

type ComposeResult<T> =
  | { status: 'generated'; generationId: string; output: T; usage: ComposeUsage }
  | { status: 'opted_out'; generationId: null; output: null; usage: null };

status: 'opted_out' is returned (as a normal 200, not an error) when the end-user has opted out of AI personalization. No model call runs and nothing is persisted. Branch on status before reading output.

Examples

Basic usage with a typed output

import { createClient } from '@vevee/sdk';

const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });

const r = await vevee.compose<{ headline: string; body: string }>('onboarding-email', userId);
if (r.status === 'opted_out') return FALLBACK;
console.log(r.output.headline);

Passing template variables

const r = await vevee.compose<{ summary: string }>(
  'release-notes',
  userId,
  { version: '2.4.0', tone: 'concise' },
);
if (r.status === 'generated') console.log(r.output.summary);

Server-side (Next.js route handler)

// app/api/onboarding/route.ts
import { createClient, VeveeError } from '@vevee/sdk';

const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });

export async function POST(req: Request) {
  const { userId } = await req.json();

  try {
    const r = await vevee.compose<{ headline: string; body: string }>(
      'onboarding-email',
      userId,
    );
    if (r.status === 'opted_out') return new Response(null, { status: 204 });
    return Response.json(r.output);
  } catch (e) {
    if (e instanceof VeveeError && e.code === 'ai_budget_exceeded') {
      return Response.json({ error: 'AI budget exhausted' }, { status: 429 });
    }
    throw e;
  }
}

Errors

Throws a typed VeveeError with one of these codes:

  • invalid_request (400) - missing/invalid type or userId.
  • invalid_key (401) - missing, malformed, or revoked API key.
  • requires_secret_key (403) - called with a pk_* key; compose needs sk_*.
  • not_found (404) - no compose type with that name exists for this app.
  • ai_budget_exceeded (429) - the workspace's AI budget is exhausted; no generation runs.
  • generation_failed (502) - the upstream model call failed.

See also