Skip to main content
The runtime call depends on what your app needs to decide. Use this rule first:
record tells Unprice what happened. consume decides whether known usage may happen now. runs reserve a budget before a workload starts. access.check asks without changing state.
Use eventSlug for the broad event your app observed, such as completions. Use featureSlug for the specific product feature you are checking or consuming, such as ai-messages, ai-generations, or ai-tools.

Decision table

Use caseCallBlocks paid work?Mutates state?Best for
Compare Unprice against existing logicaccess.checkNoNoShadow mode, preflight UI, entitlement checks
Enforce a known usage amount before work runsusage.consumeYesYesAPI calls, exports, known-cost actions
Report usage without blocking the requestusage.recordNoYes, asynchronouslyBackground metering, invoice evidence, eventual analytics
Reserve spend for multi-step workruns.start / runs.consume / runs.endYesYesWorkflows, jobs, tools, agents, custom workloads

access.check: ask without changing state

Use access.check when you want to know whether a customer can use a feature, but you do not want to consume usage or reserve credits.
const { result, error } = await unprice.access.check({
  customerId: "cus_1234567890",
  featureSlug: "ai-messages"
});

if (error) {
  throw new Error(error.message);
}

if (!result.allowed) {
  return new Response("Feature unavailable", { status: 403 });
}
Use it in shadow mode by logging result.allowed beside your current logic.

usage.consume: decide and apply now

Use usage.consume when the request path needs a synchronous commercial decision and the usage amount is known before the work runs.
const { result, error } = await unprice.usage.consume({
  customerId: "cus_1234567890",
  featureSlug: "ai-messages",
  eventSlug: "completions",
  idempotencyKey: request.id,
  properties: {
    aiMessages: 1,
    inputTokens: 1840,
    outputTokens: 620
  }
});

if (error) {
  throw new Error(error.message);
}

if (!result.allowed) {
  return new Response("Usage limit reached", { status: 429 });
}
usage.consume is the enforcing path for simple known-cost actions. Retry the same logical request with the same idempotencyKey.

usage.record: report for evidence

Use usage.record when the request should not wait for Unprice to decide. It queues the usage event for metering, analytics, and invoice evidence.
const { error } = await unprice.usage.record({
  customerId: "cus_1234567890",
  eventSlug: "completions",
  idempotencyKey: request.id,
  properties: {
    aiMessages: 1,
    aiGenerations: 2,
    aiTools: 3,
    inputTokens: 1840,
    outputTokens: 620,
    model: "gpt-4.1-mini"
  }
});

if (error) {
  console.error(error.message);
}
usage.record never blocks over-budget work. Do not use it as a spend gate. One async event can carry several measured properties; meters decide which property applies to each feature.

runs: reserve a budget envelope

Use budgeted runs when a paid action unfolds over multiple steps and can spend more than expected. The run labels the workload and reserves a budget before it starts.
// Reserve spend before the workload creates cost.
const { result: run, error } = await unprice.runs.start({
  customerId: "cus_1234567890",
  budgetAmountMinor: 5000,
  idempotencyKey: `run:${request.id}`,
  workloadType: "workflow",
  workloadId: "wf_summary_123"
});

if (error) {
  throw new Error(error.message);
}

// Only start the paid work after the reservation is running.
if (run.status !== "running") {
  return new Response(`Run rejected: ${run.status}`, { status: 429 });
}

let finalStatus: "completed" | "failed" = "completed";

try {
  // Report each billable step against the running budget.
  const { result: usage, error: consumeError } = await unprice.runs.consume({
    runId: run.runId,
    featureSlug: "ai-tools",
    eventSlug: "completions",
    idempotencyKey: `run:${request.id}:step:summary`,
    properties: {
      aiTools: 3,
      inputTokens: 1840,
      outputTokens: 620
    }
  });

  if (consumeError) {
    throw new Error(consumeError.message);
  }

  if (!usage.accepted) {
    finalStatus = "failed";
    return new Response(`Run rejected: ${usage.reason}`, { status: 429 });
  }
} catch (error) {
  finalStatus = "failed";
  throw error;
} finally {
  // Always close the run so unused reservation funds are released.
  const { error: endError } = await unprice.runs.end({
    runId: run.runId,
    status: finalStatus
  });

  if (endError) {
    console.error(endError.message);
  }
}
Call runs.consume as the workload spends and runs.end when it finishes. Unprice does not own the workload executor; it owns the budget reservation, spend decision, and evidence trail.

Shortcut

Simple request path

Use usage.consume when one request maps to a known usage amount.

Multi-step workload

Use runs.start when the workload needs a budget before it begins.