Skip to content

Billing Config

billing.config.ts is the source of truth for your Guapocado billing model.

import { defineBilling } from "@guapocado/sdk";
export default defineBilling({
entitlements: {
"advanced-analytics": { type: "feature" },
},
products: [
{
key: "pro",
entitlements: {
"advanced-analytics": true,
},
},
],
});
type BillingConfig = {
entitlements: Record<string, EntitlementDefinition>;
products: PlanDefinition[];
webhooks?: {
devTunnel?: boolean;
forwarding?: WebhookForwardingDefinition[];
};
checkout?: {
allowedRedirectHosts?: string[];
};
generate?: GenerateConfig;
};

checkout.allowedRedirectHosts is an optional, opt-in open-redirect guard. When set, the API rejects checkout.create successUrl/cancelUrl values whose host is not in the list. Only the host is checked — path and query string are preserved, so deep links and session params still work.

checkout: {
// bare hostname, or host:port (e.g. for local dev)
allowedRedirectHosts: ["app.example.com", "localhost:3000"],
}

Config is the source of truth: pushing with hosts locks redirects to them; pushing without the field clears the lock. Omit it and redirect URLs are unrestricted (current behavior).

entitlements: {
"advanced-analytics": { type: "feature" },
"api-calls": { type: "meter", reset: "monthly" },
seats: { type: "limit" },
}

Supported entitlement types:

  • feature
  • meter
  • limit

Supported meter reset values:

  • daily
  • weekly
  • monthly

Products are declared in products.

products: [
{
key: "pro",
name: "Pro",
pricing: {
mode: "recurring",
type: "flat",
amount: 4900,
currency: "usd",
frequency: "month",
},
entitlements: {
"advanced-analytics": true,
"api-calls": { included: 100000 },
seats: { included: 10 },
},
},
]

key is the product key your app uses.

name is optional display metadata.

pricing is optional. Omit pricing for free or internal products.

pricing: {
mode: "recurring",
type: "flat",
amount: 4900,
currency: "usd",
frequency: "month",
}

Supported frequencies:

  • month
  • year

amount is in the smallest currency unit. For USD, 4900 means $49.00.

pricing: {
mode: "one_time",
type: "flat",
amount: 1900,
currency: "usd",
}

Use one-time products for:

  • credit packs
  • lifetime unlocks
  • purchased limit increments
  • one-off setup fees
pricing: {
mode: "custom",
contact: "mailto:sales@example.com", // optional CTA link
}

Custom tiers have no Stripe price: they’re skipped by Stripe sync, and self-serve checkout for them returns 409 (“contact sales”). They’re sold through an enterprise deal — once a customer has a contract with an inline price, checkout for that tier succeeds at the negotiated rate. type is optional for custom tiers, and amount/frequency/interval must be omitted.

Use a custom tier for an “Enterprise — contact us” plan that you price per deal.

pricing.type describes the price shape:

  • flat
  • per_seat
  • usage

The current app-facing primitives are still entitlements. Do not use pricing.type as a replacement for feature, meter, or limit.

Feature:

"advanced-analytics": true

Meter:

"api-calls": {
included: 100000,
}

Limit:

seats: {
included: 10,
}
"api-calls": {
included: 100000,
overage: {
allowed: true,
unit: 10000,
amount: 500,
currency: "usd",
},
}

This means the product includes 100,000 API calls and can charge $5.00 per 10,000 extra calls if the customer enables overage.

When you run guap push, Guapocado creates a metered Stripe price for each overage-enabled entitlement. A customer enabling overage (usage.configure) adds a metered subscription item. Each consume() call past the included balance reports usage records to Stripe, which invoices at the end of the billing period.

If overage is not defined on the plan, usage.configure({ overageEnabled: true }) returns an error.

seats: {
included: 10,
expansion: {
allowed: true,
unit: 1,
amount: 1200,
currency: "usd",
},
}

This means the product includes 10 seats and allows extra seats at $12.00 each.

When you run guap push, Guapocado creates a licensed Stripe price for each expansion-enabled entitlement. Calling limits.configure with a purchased value adds or updates a subscription item at quantity = purchased / unit.

If expansion is not defined on the plan, limits.configure with a non-zero purchased value returns an error.

webhooks: {
devTunnel: true,
forwarding: [
{
key: "better-auth",
path: "/api/auth/guap",
events: "*",
integration: "better-auth",
autoRegister: true,
},
],
}

Use path for app-relative receivers and url for absolute receivers.

events: "*" subscribes to all Guapocado domain events.

generate: {
tables: {
enabled: true,
orm: "drizzle",
db: "sqlite",
output: "src/db/guapocado.ts",
},
}

Use this only for local read-model mode.