reserve / commit / release

The only safe way to enforce limits under concurrency. reserve() atomically holds quota with a 60-second TTL. You then commit() on success or release() on failure. Forgotten reservations auto-release on TTL.

The pattern

const r = await vevee.reserve(userId, 'image.render', 1, { model: 'flux-pro' });

if (!r.allowed) {
  // hard stop - user is at quota
  throw new LimitError(r.reasons);
}

try {
  const image = await callFluxPro(prompt);
  await vevee.commit(r.reservationId!);   // confirm the charge
  return image;
} catch (err) {
  await vevee.release(r.reservationId!);  // refund the quota
  throw err;
}

vevee.reserve()POST /api/v1/reservesk_live_

Signature

reserve(
  userId: string,
  event: string,
  quantity?: number,
  metadata?: EventMetadata,
  options?: {
    prompt?: string;        // input prompt - stored to event_logs on commit/release
  },
): Promise<ReserveResponseData>

Response

interface ReserveResponseData {
  allowed: boolean;
  matched: boolean;           // false → no limit group covers this event,
                              //   or the user has no subscription on file
  reservationId?: string;     // 'rsv_…', present when allowed=true
  expiresAt?: string;         // ISO 8601, ~60s from now
  reasons?: string[];         // 'limit_reached' | 'unmatched_event' | 'no_subscription'
}
i
Fail-closed by default.If the event_type doesn't match any limit group on the user's plan, reserve returns { allowed: false, matched: false, reasons: ['unmatched_event'] } and does not create a reservation. Same for users with no subscription on file (no_subscription). The SDK console.warns in development so typos and missing limit groups surface immediately.

vevee.commit()POST /api/v1/commitsk_live_

Confirms a reservation. Counter stays incremented; the event becomes a permanent record.

commit(
  reservationId: string,
  options?: {
    response?: string;      // model's output - stored on the event_logs row
  },
): Promise<void>

vevee.release()POST /api/v1/releasesk_live_

Cancels a reservation. Counter is decremented back; quota is returned to the user.

Signature

release(
  reservationId: string,
  options?: {
    reason?: string;      // free-form note, max 500 chars
    errorCode?: string;   // short machine-readable code, max 100 chars
    response?: string;    // partial output, if any - stored on the event_logs row
  },
): Promise<void>

Capturing why a reservation was released

Both reason and errorCodeare optional. When provided, they are persisted alongside the released reservation so you can later answer "why did this user's quota get refunded?" - useful for debugging provider failures, surfacing error rates per model, or auditing disputed charges.

try {
  const image = await callFluxPro(prompt);
  await vevee.commit(r.reservationId!);
} catch (e) {
  await vevee.release(r.reservationId!, {
    errorCode: 'provider_error',
    reason: e instanceof Error ? e.message : String(e),
  });
  throw e;
}
i
Use errorCodefor short stable identifiers you'll group on (provider_error, user_canceled, safety_filter, timeout). Use reason for the human-readable detail (the upstream error message). The server truncates to 100 / 500 characters respectively.

Capturing the prompt and response

Pass the user's prompt to reserve() and the model's response to commit() (or release(), if the call failed mid-stream). Both end up in the event_logstable tied to the resulting event, viewable in the dashboard's user detail page. Enable the feature first in Settings → Prompt logging; while it's off these fields are silently ignored.

const r = await vevee.reserve(userId, 'image.render', 1, { model: 'flux-pro' },
  { prompt },                                    // capture the input upfront
);
if (!r.allowed) throw new LimitError(r.reasons);

try {
  const image = await callFluxPro(prompt);
  await vevee.commit(r.reservationId!, { response: image.url }); // capture the output
} catch (e) {
  await vevee.release(r.reservationId!, {
    errorCode: 'provider_error',
    reason: e instanceof Error ? e.message : String(e),
    response: '',                                // optional partial output
  });
  throw e;
}
i
The prompt persists across the reservation lifecycle even when the call fails - useful for debugging which inputs trigger content filters or provider errors. Each field is capped at 32 KB server-side and truncated with …[truncated] if larger.

Reservation lifecycle

  1. reserve() - atomic increment. reservationId issued, expires in 60s.
  2. Within 60s, exactly one of:
    • commit() - final. Counter stays. Event recorded.
    • release() - counter decremented. No event recorded.
    • (timeout) - server auto-releases on TTL.

Real-world example

// app/api/generate/route.ts (Next.js)
import { vevee } from '@/lib/vevee';
import { VeveeError } from '@vevee/sdk';

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

  let reservationId: string | undefined;

  try {
    const r = await vevee.reserve(userId, 'image.render', 1, { model: 'flux-pro' });
    if (!r.allowed) {
      return Response.json(
        { error: 'limit_reached', reasons: r.reasons },
        { status: 429 },
      );
    }
    reservationId = r.reservationId;

    const image = await callFluxPro(prompt);
    await vevee.commit(reservationId!);
    return Response.json({ image });
  } catch (err) {
    if (reservationId) {
      await vevee.release(reservationId).catch(() => {});
    }
    if (err instanceof VeveeError) {
      return Response.json({ error: err.code, message: err.message }, { status: err.status });
    }
    throw err;
  }
}

Errors

  • limit_reached (429) - only on reserve() when at quota. The body contains { allowed: false, reasons }; this is returned as a successful HTTP response, not a thrown error, since it's an expected outcome.
  • reservation_not_pending (400) - commit or release called on a reservation that was already committed or released.
  • reservation_expired (400) - commit or release called after the 60-second TTL.
  • not_found (404) - unknown reservationId.
i
Calling release()after the auto-release timeout is harmless - you'll get reservation_expired. Wrap your release call in a .catch(() => {})if you don't care.