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;
}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'
}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.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>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;
}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;
}…[truncated] if larger.Reservation lifecycle
- reserve() - atomic increment.
reservationIdissued, expires in 60s. - 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 onreserve()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) -commitorreleasecalled on a reservation that was already committed or released.reservation_expired(400) -commitorreleasecalled after the 60-second TTL.not_found(404) - unknownreservationId.
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.