Owostack

Create plans & features

Reference: metered(), boolean(), entity(), creditSystem(), creditPack(), plan() — define features and plans as code

Create plans & features

Builder functions for defining features and plans declaratively. Features become handles you can use directly for .check() and .track().

metered(slug, opts?)

Create a metered feature handle. Metered features have numeric limits that reset on a schedule.

import { metered } from "owostack";

const apiCalls = metered("api-calls", { name: "API Calls" });

Parameters

ParameterTypeRequiredDescription
slugstringYesUnique feature identifier
opts.namestringNoHuman-readable name (auto-generated from slug if omitted)

Methods

.limit(value, config?)

Create a plan feature entry with a specific limit.

apiCalls.limit(1000);
apiCalls.limit(50000, { reset: "daily" });
apiCalls.limit(50000, {
  overage: "charge",
  overagePrice: 100,
  maxOverageUnits: 100000,
});
ParameterTypeDefaultDescription
valuenumberUsage limit per period
config.resetResetInterval"monthly"Reset schedule
config.usageModel"included" | "usage_based" | "prepaid""included"Entitlement model for the feature
config.pricePerUnitnumberPackage or per-unit price (minor currency)
config.ratingModel"package" | "graduated" | "volume""package"How billable usage is priced
config.tiersPricingTier[]Tier definitions for graduated or volume pricing
config.overage"block" | "charge""block"What happens when limit is exceeded
config.overagePricenumberPrice per overage unit (minor currency)
config.maxOverageUnitsnumberHard cap on overage units per period
config.billingUnitsnumber1Package size for billing
config.creditCostnumberCost in credits per unit

.trialLimit(value)

Set a different usage limit for the trial period. When a customer is in a trial, this limit applies instead of the regular limit.

apiCalls.limit(10000).trialLimit(1000);
ParameterTypeRequiredDescription
valuenumberYesTrial usage limit (null = unlimited)

Example: Give customers 1,000 API calls during their 14-day trial, then 10,000 calls per month once they convert:

plan("pro", {
  name: "Pro",
  price: 2000,
  currency: "USD",
  interval: "monthly",
  trialDays: 14,
  features: [apiCalls.limit(10000).trialLimit(1000)],
});

If you don't set a trial limit, the regular limit applies during both trial and active periods.

.unlimited()

Create a plan feature entry with no limit.

apiCalls.unlimited();

.perUnit(unitPrice, config?)

Create a usage-based feature that bills from the first tracked unit.

apiCalls.perUnit(2); // $0.02 per unit
apiCalls.perUnit(500, { reset: "monthly" }); // $5.00 per unit

perUnit() is a convenience wrapper for usageModel: "usage_based" with ratingModel: "package" and billingUnits: 1.

.graduated(tiers, config?)

Create stacked tier pricing where each tier rates only the units inside that tier.

apiCalls.graduated([
  { upTo: 1000, unitPrice: 0 },
  { upTo: 10000, unitPrice: 10 },
  { upTo: null, unitPrice: 5 },
]);

For 1,500 units, the first 1,000 units use tier 1 and the next 500 units use tier 2.

If a tier includes flatFee, that fee is added once when usage enters that tier. Graduated pricing is cumulative across tiers used.

Example with billable usage of 31:

apiCalls.graduated([
  { upTo: 30, flatFee: 1000 },
  { upTo: 100, flatFee: 5000 },
]);

Result:

  • units 1-30 enter tier 1, so tier 1 adds 1000
  • unit 31 enters tier 2, so tier 2 adds 5000
  • total = 6000

.volume(tiers, config?)

Create all-units pricing where the reached tier applies to all billable usage.

apiCalls.volume([
  { upTo: 1000, unitPrice: 100 },
  { upTo: 10000, unitPrice: 80 },
  { upTo: null, unitPrice: 50 },
]);

For 1,500 units, all 1,500 units are billed at tier 2's unitPrice.

If a tier includes flatFee, only the reached tier's flatFee is applied. Volume pricing is not cumulative across multiple tiers.

Example with billable usage of 31:

apiCalls.volume([
  { upTo: 30, flatFee: 1000 },
  { upTo: 100, flatFee: 5000 },
]);

Result:

  • usage reaches tier 2
  • only tier 2 applies
  • total = 5000

Graduated vs volume

Both models use tiers, but they answer different pricing questions:

  • graduated: "what price applies to each chunk of usage?"
  • volume: "what price applies to the total after I know the final band?"

For the same 31 billable units:

const tiers = [
  { upTo: 30, unitPrice: 100 },
  { upTo: 100, unitPrice: 50 },
];
  • graduated = 30 * 100 + 1 * 50 = 3050
  • volume = 31 * 50 = 1550

Use graduated when pricing should accumulate step by step. Use volume when crossing into a lower band should reprice all billable usage at that band's rate.

.config(opts)

Create a plan feature entry with full config object.

apiCalls.config({
  limit: 50000,
  reset: "monthly",
  overage: "charge",
  ratingModel: "graduated",
  tiers: [
    { upTo: 10000, unitPrice: 10 },
    { upTo: null, unitPrice: 5 },
  ],
});

.check(customer, opts?)

Check if a customer has access to this feature.

const result = await apiCalls.check("user_123");
const result = await apiCalls.check("user_123", { value: 5 });
const result = await apiCalls.check("user_123", { sendEvent: true });

Returns Promise<CheckResult>.

.track(customer, value?, opts?)

Track usage for this feature.

await apiCalls.track("user_123");
await apiCalls.track("user_123", 5);
await apiCalls.track("user_123", 1, { entity: "workspace_123" });

Returns Promise<TrackResult>.


boolean(slug, opts?)

Create a boolean feature handle. Boolean features are either enabled or disabled per plan — no usage tracking.

import { boolean } from "owostack";

const analytics = boolean("analytics", { name: "Analytics Dashboard" });

Parameters

Same as metered().

Methods

.on()

Feature is included in this plan.

analytics.on();

.off()

Feature is NOT included in this plan.

analytics.off();

.check(customer, opts?)

Check if a customer has access to this feature.

const { allowed } = await analytics.check("user_123");

Returns Promise<CheckResult>.

Boolean features have no .track() method — the type system enforces this.


entity(slug, opts?)

Create a non-consumable feature handle. Entity features represent persistent resources like seats, projects, or workspaces — things you add and remove to consume capacity, not counters that reset.

import { entity } from "owostack";

const seats = entity("seats", { name: "Team Seats" });
const projects = entity("projects", { name: "Projects" });

Parameters

Same as metered().

Plan methods

.limit(value, config?)

Set the maximum number of entities allowed.

seats.limit(5);
seats.limit(5, { overage: "charge", overagePrice: 500 });
ParameterTypeDefaultDescription
valuenumberMaximum entities allowed
config.overage"block" | "charge""block"What happens when limit is exceeded
config.overagePricenumberPrice per extra entity (minor currency)

Entity features default to reset: "never" — the count reflects current state, not periodic usage.

.unlimited()

No limit on the number of entities.

seats.unlimited();

Runtime methods

.add(customer, opts)

Add an entity (e.g., a seat). Validates against the plan limit.

const result = await seats.add("org@acme.com", {
  entity: "user_123",
  name: "John Doe",
  email: "john@acme.com",
  metadata: { role: "admin" },
});

console.log(result.count); // 3
console.log(result.remaining); // 2
ParameterTypeRequiredDescription
customerstringYesCustomer ID or email
opts.entitystringYesUnique entity ID
opts.namestringNoDisplay name
opts.emailstringNoEmail address
opts.metadataRecord<string, unknown>NoCustom metadata

Returns Promise<AddEntityResult>.

.remove(customer, entity)

Remove an entity. Frees the slot immediately.

await seats.remove("org@acme.com", "user_123");

Returns Promise<RemoveEntityResult>.

.list(customer)

List all active entities for this feature.

const { entities, total } = await seats.list("org@acme.com");

for (const seat of entities) {
  console.log(seat.id, seat.name, seat.status);
}

Returns Promise<ListEntitiesResult>.

.check(customer, opts?)

Check how many entities exist and if more can be added.

const result = await seats.check("org@acme.com");
console.log(result.allowed); // true if under limit
console.log(result.balance); // remaining slots

Returns Promise<CheckResult>.

Entity features have no .track() method — entities are managed via .add() and .remove(). The type system enforces this.


creditSystem(slug, config)

Create a credit system definition. A credit system abstracts usage across multiple features into a single "credit" balance.

import { creditSystem, metered } from "owostack";

const gpt4 = metered("gpt-4");
const dallE = metered("dall-e");

const aiCredits = creditSystem("ai-credits", {
  name: "AI Credits",
  features: [
    gpt4(20), // GPT-4 costs 20 credits per track()
    dallE(50), // Dall-E costs 50 credits per track()
  ],
});

Parameters

ParameterTypeRequiredDescription
slugstringYesUnique credit system identifier
config.namestringNoHuman-readable name
config.descriptionstringNoDescription
config.featuresFeatureHandle[]YesArray of features that consume from this system

Methods

.credits(amount, config?)

Include a specific amount of credits for this system in a plan.

aiCredits.credits(1000, { reset: "monthly", overage: "charge" });

creditPack(slug, config)

Create a credit pack definition. Credit packs are one-time purchasable bundles of credits that top up a customer's prepaid balance. They're ideal for letting customers buy additional usage beyond their plan limits.

import { creditPack, creditSystem } from "owostack";

const apiCredits = creditSystem("api-credits", {
  name: "API Credits",
  features: [apiCalls(1)],
});

const starterPack = creditPack("starter-pack", {
  name: "Starter Pack",
  description: "500 API credits",
  credits: 500,
  price: 1000, // $10.00
  currency: "USD",
  creditSystem: "api-credits", // Must match a credit system slug
});

const proPack = creditPack("pro-pack", {
  name: "Pro Pack",
  description: "2500 API credits",
  credits: 2500,
  price: 3500, // $35.00
  currency: "USD",
  creditSystem: "api-credits",
});

export const owo = new Owostack({
  secretKey: process.env.OWOSTACK_SECRET_KEY!,
  catalog: [
    apiCredits,
    starterPack,
    proPack,
    // ... your plans
  ],
});

Parameters

ParameterTypeRequiredDescription
slugstringYesUnique credit pack identifier
config.namestringYesHuman-readable pack name
config.descriptionstringNoPack description shown to customers
config.creditsnumberYesNumber of credits in the pack
config.pricenumberYesPrice in minor currency units (e.g. 1000 = $10.00)
config.currencyCurrencyYesCurrency code ("NGN", "USD", etc.)
config.creditSystemstringYesSlug of the credit system this pack is tied to
config.providerstringNoOverride the default payment provider
config.metadataRecord<string, unknown>NoCustom metadata

Methods

Credit packs don't have methods like features do. Instead, they're purchased using the addon() method:

// Customer buys a credit pack
const result = await owo.addon({
  customer: "user_123",
  pack: "starter-pack",
});

if (result.requiresCheckout) {
  // Redirect to payment
  res.redirect(result.checkoutUrl!);
} else {
  console.log(`Added ${result.credits} credits`);
  console.log(`New balance: ${result.balance}`);
}

plan(slug, config)

Create a plan definition for the catalog.

import { plan } from "owostack";

plan("pro", {
  name: "Pro",
  price: 500000,
  currency: "NGN",
  interval: "monthly",
  features: [
    apiCalls.limit(50000, { overage: "charge", overagePrice: 100 }),
    aiCredits.credits(1000),
    analytics.on(),
    seats.limit(20),
  ],
});

Parameters

ParameterTypeRequiredDescription
slugstringYesUnique plan identifier
config.namestringYesHuman-readable plan name
config.pricenumberYesPrice in minor currency units (e.g. kobo)
config.currencyCurrencyYesCurrency code ("NGN", "GHS", "ZAR", "KES", "USD")
config.intervalPlanIntervalYesBilling interval ("monthly", "yearly", etc.)
config.featuresPlanFeatureEntry[]YesArray of feature entries from .limit(), .credits(), etc.
config.descriptionstringNoPlan description
config.planGroupstringNoGroup for upgrade/downgrade logic
config.trialDaysnumberNoTrial period in days
config.autoEnablebooleanNoAuto-assign this plan to new customers (default: false)
config.metadataRecord<string, unknown>NoCustom metadata

owo.sync()

Push the catalog to the API. Creates and updates features, credit systems, and plans.

const result = await owo.sync();

Returns Promise<SyncResult>

interface SyncResult {
  success: boolean;
  features: { created: string[]; updated: string[]; unchanged: string[] };
  creditSystems: { created: string[]; updated: string[]; unchanged: string[] };
  plans: { created: string[]; updated: string[]; unchanged: string[] };
  warnings: string[];
}

CLI: owo sync

Push a catalog from the command line.

npx owosk sync [options]
OptionDescription
--config <path>Path to config file (default: ./owo.config.ts)
--dry-runShow what would change without applying
--key <api-key>API secret key (or set OWOSTACK_SECRET_KEY env var)
--url <api-url>API URL override

Example config file

// owo.config.ts
import {
  Owostack,
  metered,
  boolean,
  entity,
  creditSystem,
  plan,
} from "owostack";

const apiCalls = metered("api-calls");
const aiCredits = creditSystem("ai-credits", {
  features: [apiCalls(1)],
});

export default new Owostack({
  secretKey: process.env.OWOSTACK_SECRET_KEY!,
  catalog: [
    plan("starter", {
      name: "Starter",
      price: 0,
      currency: "NGN",
      interval: "monthly",
      features: [aiCredits.credits(1000)],
    }),
    plan("pro", {
      name: "Pro",
      price: 500000,
      currency: "NGN",
      interval: "monthly",
      features: [aiCredits.credits(50000)],
    }),
  ],
});

On this page

AI Chat

Owostack docs assistant

Start a new chat below.