# AI Onboarding (/docs/ai-onboarding)
If you're developing with AI, Commet offers several resources to improve your experience.
* [Commet MCP Server](#commet-mcp-server)
* [Commet Docs for Agents](#commet-docs-for-agents)
* [Billing Skills for Agents](#billing-skills-for-agents)
Prerequisite: Create an Account [#prerequisite-create-an-account]
Currently, we require a human to create a Commet account at [commet.co](https://commet.co). The MCP server uses OAuth for authentication — your agent will be guided through the authorization flow automatically when connecting.
Commet MCP Server [#commet-mcp-server]
MCP is an open protocol that standardizes how applications provide context to LLMs. Among other benefits, it provides LLMs tools to act on your behalf. Our MCP server covers the full Commet platform and uses OAuth for authentication.
The Commet MCP server can be added to any supported MCP client:
| Environment | URL |
| -------------- | ------------------------------- |
| **Production** | `https://commet.co/mcp` |
| **Sandbox** | `https://sandbox.commet.co/mcp` |
For example:
```json
{
"mcpServers": {
"commet": {
"url": "https://sandbox.commet.co/mcp"
}
}
}
```
Replace with `https://commet.co/mcp` when you're ready to work with live data.
Commet Docs for Agents [#commet-docs-for-agents]
You can give your agent current docs in a context-aware way:
1. **Full llms.txt**
Give your agent all our docs in a single file.
```
https://commet.co/llms-full.txt
```
2. **Per-page markdown**
Every doc includes a markdown version. Append `.mdx` to any docs path.
```
https://commet.co/docs/ai-onboarding.mdx
```
Billing Skills for Agents [#billing-skills-for-agents]
Agent Skills give AI agents expert-level billing knowledge — SDK integration, pricing models, subscription patterns, and best practices.
Install all Commet skills:
```bash
npx skills add commet-labs/commet-skills
```
Or install standalone knowledge packages:
```bash
npx skills add commet-labs/billing-best-practices
npx skills add commet-labs/pricing-models
npx skills add commet-labs/subscription-patterns
```
# Billing Best Practices Skill (/docs/billing-best-practices-skill)
The Billing Best Practices skill provides AI agents with comprehensive knowledge for building production-ready billing integrations. It covers subscription lifecycle, failed payments, proration, multi-currency, tax compliance, and invoicing.
Installation [#installation]
Install the skill using the following command:
```bash
npx skills add commet-labs/billing-best-practices
```
Advantages [#advantages]
* **Subscription lifecycle management**: Patterns for handling every state transition from trial to cancellation.
* **Failed payment recovery**: Dunning flows, retry schedules, grace periods, and involuntary churn prevention.
* **Proration logic**: How mid-cycle upgrades, downgrades, and plan changes affect invoicing.
* **Multi-currency support**: Regional pricing, currency detection from billing address, zero-decimal currencies.
* **Tax compliance**: Merchant of Record model, automatic tax calculation, when you need MoR vs DIY.
* **Pre-launch checklist**: Step-by-step verification before going live with billing.
Learn More [#learn-more]
# Commet Skills (/docs/commet-skill)
Agent Skills give AI agents modular billing capabilities — SDK integration, pricing models, subscription patterns, and billing best practices. Install all skills with a single command.
Install all skills [#install-all-skills]
```bash
npx skills add commet-labs/commet-skills
```
Install a single skill [#install-a-single-skill]
```bash
npx skills add commet-labs/commet-skills --skill commet
npx skills add commet-labs/commet-skills --skill ai-billing
npx skills add commet-labs/commet-skills --skill billing-behaviors
npx skills add commet-labs/commet-skills --skill commet-webhooks
npx skills add commet-labs/commet-skills --skill commet-cli
```
Available skills [#available-skills]
| Skill | Description |
| ------------------- | -------------------------------------------------------------------------- |
| `commet` | Core SDK — @commet/node, @commet/next, @commet/ai-sdk, @commet/better-auth |
| `billing-behaviors` | Business rules — proration, plan changes, subscription lifecycle |
| `commet-cli` | CLI — login, link, pull types, scaffold from templates |
| `commet-webhooks` | Webhooks — event handling, signature verification, framework handlers |
| `ai-billing` | AI billing — tracked() middleware, balance model, cost calculation |
Standalone skills [#standalone-skills]
Universal billing knowledge that works with any stack. Code examples use `@commet/node`.
```bash
npx skills add commet-labs/billing-best-practices
npx skills add commet-labs/pricing-models
npx skills add commet-labs/subscription-patterns
```
Supported agents [#supported-agents]
Claude Code, Cursor, Codex, Gemini CLI, GitHub Copilot, Windsurf, OpenCode, and 40+ more.
Learn More [#learn-more]
# MCP Server (/docs/mcp-server)
What is an MCP Server? [#what-is-an-mcp-server]
MCP is an open protocol that standardizes how applications provide context to LLMs. Among other benefits, it provides LLMs tools to act on your behalf.
What can Commet's MCP Server do? [#what-can-commets-mcp-server-do]
Commet's MCP server gives your AI agent native access to the full Commet platform through a single integration. You can manage all aspects of your billing infrastructure using natural language.
* **Organizations** — List, create, switch, and get current organization
* **Plans** — Create, update, delete, and toggle visibility of plans. Add and remove prices and features
* **Features** — Create, update, delete features and seat types
* **Customers** — List, search, create, and update customers. View their subscriptions, invoices, and usage
* **Subscriptions** — Search subscriptions, view details, invoices, and transactions
* **Invoices** — Search invoices, view with line items, get billing summary
* **Usage** — View usage summary by feature or by customer
* **Transactions** — List recent transactions, view financial summary
* **Addons** — Create, update, and archive add-ons linked to features
* **Promo Codes** — Toggle promo codes between active and inactive
* **API Keys** — Create and delete API keys for SDK integration
As an example, you could use this to create a full billing setup, manage plans and features, inspect customer subscriptions, or review invoices and usage data.
Prerequisites [#prerequisites]
Commet provides two MCP server environments:
| Environment | URL | Use case |
| -------------- | ------------------------------- | ----------------------------------- |
| **Production** | `https://commet.co/mcp` | Live billing data |
| **Sandbox** | `https://sandbox.commet.co/mcp` | Testing without affecting real data |
To use it, you'll need to:
* [Create a Commet account](https://commet.co)
* Have an MCP-compatible client (Cursor, Claude Code, Claude Desktop, etc.)
No API key is needed. Authentication happens automatically through OAuth when your client connects.
Use the sandbox environment when experimenting or setting up billing for the first time. Production changes affect real customers immediately.
How to use the MCP Server [#how-to-use-the-mcp-server]
Choose your preferred client below.
Open the command palette and choose "Cursor Settings" > "MCP" > "Add new global MCP server".
Production [#production]
```json
{
"mcpServers": {
"commet": {
"url": "https://commet.co/mcp"
}
}
}
```
Sandbox [#sandbox]
```json
{
"mcpServers": {
"commet-sandbox": {
"url": "https://sandbox.commet.co/mcp"
}
}
}
```
Production [#production-1]
```bash
claude mcp add --transport http commet https://commet.co/mcp
```
Sandbox [#sandbox-1]
```bash
claude mcp add --transport http commet-sandbox https://sandbox.commet.co/mcp
```
Open Claude Desktop settings > "Developer" tab > "Edit Config".
Production [#production-2]
```json
{
"mcpServers": {
"commet": {
"url": "https://commet.co/mcp"
}
}
}
```
Sandbox [#sandbox-2]
```json
{
"mcpServers": {
"commet-sandbox": {
"url": "https://sandbox.commet.co/mcp"
}
}
}
```
Production [#production-3]
```bash
codex mcp add commet --url https://commet.co/mcp
```
Sandbox [#sandbox-3]
```bash
codex mcp add commet-sandbox --url https://sandbox.commet.co/mcp
```
Each command opens the browser for OAuth authentication. Once complete, verify with:
```bash
codex mcp list
```
Add the following to your VS Code `settings.json`:
Production [#production-4]
```json
{
"mcp": {
"servers": {
"commet": {
"type": "http",
"url": "https://commet.co/mcp"
}
}
}
}
```
Sandbox [#sandbox-4]
```json
{
"mcp": {
"servers": {
"commet-sandbox": {
"type": "http",
"url": "https://sandbox.commet.co/mcp"
}
}
}
}
```
Add the following to `~/.gemini/settings.json`:
Production [#production-5]
```json
{
"mcpServers": {
"commet": {
"httpUrl": "https://commet.co/mcp"
}
}
}
```
Sandbox [#sandbox-5]
```json
{
"mcpServers": {
"commet-sandbox": {
"httpUrl": "https://sandbox.commet.co/mcp"
}
}
}
```
Add the following to `opencode.json`:
Production [#production-6]
```json
{
"mcp": {
"commet": {
"type": "remote",
"url": "https://commet.co/mcp"
}
}
}
```
Sandbox [#sandbox-6]
```json
{
"mcp": {
"commet-sandbox": {
"type": "remote",
"url": "https://sandbox.commet.co/mcp"
}
}
}
```
Production [#production-7]
```json
{
"mcpServers": {
"commet": {
"serverUrl": "https://commet.co/mcp"
}
}
}
```
Sandbox [#sandbox-7]
```json
{
"mcpServers": {
"commet-sandbox": {
"serverUrl": "https://sandbox.commet.co/mcp"
}
}
}
```
Available Tools [#available-tools]
The MCP server exposes the following tools once connected:
Organizations [#organizations]
| Tool | Description |
| -------------------------- | ----------------------------------------- |
| `list-organizations` | List all organizations you have access to |
| `get-current-organization` | Get the currently active organization |
| `switch-organization` | Switch to a different organization |
| `create-organization` | Create a new organization |
Plans & Features [#plans--features]
| Tool | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `list-plans` | List all plans in the current organization |
| `create-plan` | Create a new plan |
| `update-plan` | Update a plan's name or description |
| `delete-plan` | Soft-delete a plan. Cannot delete a plan that has subscriptions |
| `toggle-plan-visibility` | Toggle a plan between public and private. Private plans are hidden but existing subscriptions keep access |
| `add-plan-price` | Add a price to a plan |
| `add-plan-feature` | Add a feature to a plan |
| `remove-plan-feature` | Remove a feature from a plan |
| `list-features` | List all features in the current organization |
| `create-feature` | Create a new feature |
| `update-feature` | Update a feature's name, description, or unit name |
| `delete-feature` | Soft-delete a feature. Must be removed from all plans first |
| `create-seat-type` | Create a new seat type |
| `update-seat-type` | Update a seat type's name or description |
| `delete-seat-type` | Soft-delete a seat type. Must be removed from all features first |
| `list-seat-types` | List all seat types |
Customers [#customers]
| Tool | Description |
| ---------------------------- | ---------------------------------------------------- |
| `list-customers` | List all customers |
| `search-customers` | Search customers by name or email |
| `create-customer` | Create a new customer |
| `update-customer` | Update a customer's billing email, name, or timezone |
| `get-customer` | Get customer details with address |
| `get-customer-subscriptions` | View a customer's subscriptions |
| `get-customer-invoices` | View a customer's invoices |
| `get-customer-usage` | View a customer's usage data |
Subscriptions [#subscriptions]
| Tool | Description |
| ------------------------------- | ------------------------------------------------------ |
| `search-subscriptions` | Search subscriptions by name, customer, or plan |
| `get-subscription` | Get full subscription details with balance and pricing |
| `get-subscription-invoices` | View invoices for a subscription |
| `get-subscription-transactions` | View payment transactions for a subscription |
Invoices & Billing [#invoices--billing]
| Tool | Description |
| --------------------- | --------------------------------------------- |
| `search-invoices` | Search invoices by number or filter by status |
| `get-invoice` | Get invoice with line items |
| `get-invoice-summary` | Get billing summary by status |
Usage & Transactions [#usage--transactions]
| Tool | Description |
| ------------------------- | ---------------------------------------------------- |
| `get-usage-summary` | Get usage summary aggregated by feature |
| `list-transactions` | List recent transactions with optional status filter |
| `get-transaction-summary` | Get financial transaction summary |
Addons [#addons]
| Tool | Description |
| -------------- | ---------------------------------------------------------------------------------- |
| `create-addon` | Create a new add-on linked to a feature with its own pricing and consumption model |
| `update-addon` | Update an add-on's name, description, or base price |
| `delete-addon` | Archive (soft-delete) an add-on. Cannot archive with active subscriptions |
Promo Codes [#promo-codes]
| Tool | Description |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `toggle-promo-code` | Toggle a promo code between active and inactive. Inactive codes cannot be redeemed at checkout |
API Keys [#api-keys]
| Tool | Description |
| ---------------- | --------------------------------------------------------------------------------- |
| `create-api-key` | Create an API key for SDK integration |
| `delete-api-key` | Permanently delete an API key. Applications using it will immediately lose access |
# Pricing Models Skill (/docs/pricing-models-skill)
The Pricing Models skill helps AI agents choose and implement the right pricing model for any SaaS product. It covers metered, credits, balance, seats, and boolean models with decision frameworks and implementation patterns.
Installation [#installation]
Install the skill using the following command:
```bash
npx skills add commet-labs/pricing-models
```
Advantages [#advantages]
* **Decision framework**: Three-question flowchart to pick the right model for any product.
* **Metered billing**: Pay-per-use with included amounts and overage pricing.
* **Credits model**: Prepaid blocks that stop when exhausted — ideal for generation-based products.
* **Balance model**: Dollar-denominated prepaid spend — ideal for AI token billing.
* **Seat-based pricing**: Per-user billing with hybrid advance and true-up charging.
* **Hybrid patterns**: Combining models — base plan + metered, seats + usage, addons.
Learn More [#learn-more]
# Subscription Patterns Skill (/docs/subscription-patterns-skill)
The Subscription Patterns skill provides AI agents with universal patterns for the full subscription lifecycle. It covers trials, intro offers, upgrades, downgrades, proration, dunning, cancellation flows, and add-ons.
Installation [#installation]
Install the skill using the following command:
```bash
npx skills add commet-labs/subscription-patterns
```
Advantages [#advantages]
* **Free trials and intro offers**: Trial setup, conversion flows, discounted first cycles, eligibility rules.
* **Upgrades and downgrades**: When to apply immediately vs at renewal, feature access during transition.
* **Proration logic**: Time-based credit formula, edge cases, multiple changes in the same period.
* **Dunning and retries**: Failed payment recovery, retry schedules, grace period access, notification patterns.
* **Cancellation flows**: Immediate vs end-of-period, save offers, reactivation, data retention.
* **Add-ons**: Purchasable feature extensions, prorated activation, consumption model compatibility.
Learn More [#learn-more]
# Better Auth (/docs/better-auth)
[Better Auth](https://better-auth.com) is a modern authentication library for TypeScript. This plugin integrates Commet directly into your Better Auth setup.
Features [#features]
* Automatic customer creation on signup
* Customer Portal for self-service billing management
* Subscription management (get, cancel)
* Feature access control (boolean, metered, seats)
* Usage tracking for metered billing
* Seat management for per-user pricing
* Optional webhook handling with signature verification
Installation [#installation]
```bash
pnpm add better-auth @commet/better-auth @commet/node
```
```bash
npm install better-auth @commet/better-auth @commet/node
```
```bash
yarn add better-auth @commet/better-auth @commet/node
```
```bash
bun add better-auth @commet/better-auth @commet/node
```
Preparation [#preparation]
Get your API key from the [Commet dashboard](https://sandbox.commet.co).
```bash title=".env"
COMMET_API_KEY=ck_...
COMMET_ENVIRONMENT=sandbox # or production
```
Server Configuration [#server-configuration]
```typescript title="auth.ts"
import { betterAuth } from "better-auth";
import {
commet,
portal,
subscriptions,
features,
usage,
seats,
} from "@commet/better-auth";
import { Commet } from "@commet/node";
const commetClient = new Commet({
apiKey: process.env.COMMET_API_KEY,
environment: process.env.COMMET_ENVIRONMENT, // 'sandbox' or 'production'
});
export const auth = betterAuth({
// ... your config
plugins: [
commet({
client: commetClient,
createCustomerOnSignUp: true,
use: [
portal(),
subscriptions(),
features(),
usage(),
seats(),
],
}),
],
});
```
Client Configuration [#client-configuration]
```typescript title="auth-client.ts"
import { createAuthClient } from "better-auth/react";
import { commetClient } from "@commet/better-auth";
export const authClient = createAuthClient({
plugins: [commetClient()],
});
```
Configuration Options [#configuration-options]
```typescript
commet({
client: commetClient, // Required: Commet SDK instance
createCustomerOnSignUp: true, // Auto-create customer on signup
getCustomerCreateParams: ({ user }) => ({
legalName: user.name,
metadata: { source: "web" },
}),
use: [/* plugins */],
})
```
When `createCustomerOnSignUp` is enabled, a Commet customer is automatically created using the user's ID as the `customerId`. No database mapping required.
Portal Plugin [#portal-plugin]
Redirects users to the Commet customer portal for self-service billing management.
```typescript title="Server"
import { commet, portal } from "@commet/better-auth";
commet({
client: commetClient,
use: [
portal({ returnUrl: "/dashboard" }),
],
})
```
```typescript title="Client"
// Redirects to Commet customer portal
await authClient.customer.portal();
```
Subscriptions Plugin [#subscriptions-plugin]
Manage customer subscriptions.
```typescript title="Server"
import { commet, subscriptions } from "@commet/better-auth";
commet({
client: commetClient,
use: [subscriptions()],
})
```
```typescript title="Client"
// Get current subscription
const { data: subscription } = await authClient.subscription.get();
// Cancel subscription
await authClient.subscription.cancel({
subscriptionId: "sub_xxx",
reason: "Too expensive",
immediate: false, // Cancel at period end
});
```
Features Plugin [#features-plugin]
Check feature access for the authenticated user.
```typescript title="Server"
import { commet, features } from "@commet/better-auth";
commet({
client: commetClient,
use: [features()],
})
```
```typescript title="Client"
// List all features
const { data: featuresList } = await authClient.features.list();
// Get specific feature
const { data: feature } = await authClient.features.get("api_calls");
// Check if feature is enabled (boolean)
const { data: check } = await authClient.features.check("sso");
// Check if user can use one more unit (metered)
const { data: canUse } = await authClient.features.canUse("api_calls");
// Returns: { allowed: boolean, willBeCharged: boolean }
```
Usage Plugin [#usage-plugin]
Track usage events for metered billing.
```typescript title="Server"
import { commet, usage } from "@commet/better-auth";
commet({
client: commetClient,
use: [usage()],
})
```
```typescript title="Client"
await authClient.usage.track({
feature: "api_calls",
value: 1,
idempotencyKey: `evt_${Date.now()}`
});
```
The authenticated user is automatically associated with the event.
Seats Plugin [#seats-plugin]
Manage seat-based licenses.
```typescript title="Server"
import { commet, seats } from "@commet/better-auth";
commet({
client: commetClient,
use: [seats()],
})
```
```typescript title="Client"
// List all seat balances
const { data: seatBalances } = await authClient.seats.list();
// Add seats
await authClient.seats.add({ seatType: "member", count: 5 });
// Remove seats
await authClient.seats.remove({ seatType: "member", count: 2 });
// Set exact count
await authClient.seats.set({ seatType: "admin", count: 3 });
// Set all seat types at once
await authClient.seats.setAll({ admin: 2, member: 10, viewer: 50 });
```
Webhooks Plugin [#webhooks-plugin]
Handle Commet webhooks. This is optional since you can always query state directly.
```typescript title="Server"
import { commet, webhooks } from "@commet/better-auth";
commet({
client: commetClient,
use: [
webhooks({
secret: process.env.COMMET_WEBHOOK_SECRET,
onPayload: (payload) => {
// Catch-all handler
},
onSubscriptionCreated: (payload) => {},
onSubscriptionActivated: (payload) => {},
onSubscriptionCanceled: (payload) => {},
onSubscriptionUpdated: (payload) => {},
}),
],
})
```
Configure the webhook endpoint in your Commet dashboard: `/api/auth/commet/webhooks`
Full Example [#full-example]
Server Setup [#server-setup]
```typescript title="auth.ts"
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import {
commet as commetPlugin,
portal,
subscriptions,
features,
usage,
seats,
} from "@commet/better-auth";
import { Commet } from "@commet/node";
import { db } from "./db";
import * as schema from "./schema";
const commetClient = new Commet({
apiKey: process.env.COMMET_API_KEY!,
environment: process.env.COMMET_ENVIRONMENT as "sandbox" | "production",
});
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg", schema }),
emailAndPassword: { enabled: true },
plugins: [
commetPlugin({
client: commetClient,
createCustomerOnSignUp: true,
getCustomerCreateParams: ({ user }) => ({
legalName: user.name,
}),
use: [
portal({ returnUrl: "/dashboard" }),
subscriptions(),
features(),
usage(),
seats(),
],
}),
],
});
```
Client Setup [#client-setup]
```typescript title="auth-client.ts"
import { createAuthClient } from "better-auth/react";
import { commetClient } from "@commet/better-auth";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
plugins: [commetClient()],
});
export const { signIn, signUp, signOut, useSession } = authClient;
```
Usage in Components [#usage-in-components]
```tsx title="dashboard.tsx"
"use client";
import { authClient } from "@/lib/auth-client";
export function BillingSection() {
const handlePortal = async () => {
await authClient.customer.portal();
};
const checkFeature = async () => {
const { data } = await authClient.features.canUse("api_calls");
if (data?.allowed) {
// Proceed with action
await authClient.usage.track({ feature: "api_calls" });
}
};
return (
);
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Usage Events](/docs/track-usage)
* [Seat Management](/docs/seat-management)
* [Customer Portal](/docs/customer-portal)
# CLI (/docs/cli)
Generate TypeScript types from your Commet dashboard for autocomplete in your code. Requires Node.js 18+.
Install [#install]
```bash
pnpm add -g commet
```
```bash
npm install -g commet
```
```bash
yarn global add commet
```
Setup [#setup]
```bash
commet login # Authenticate in browser
commet link # Link project to organization
commet pull # Generate TypeScript types
```
After running `commet pull`, your SDK calls get autocomplete:
```typescript
await commet.usage.track({
customerId: 'user_123',
feature: 'api_calls', // autocomplete
})
await commet.subscriptions.create({
customerId: 'user_123',
planCode: 'pro', // autocomplete
})
```
Commands [#commands]
| Command | Description |
| ---------------------- | ----------------------------------------- |
| `commet login` | Authenticate with Commet (opens browser) |
| `commet logout` | Remove credentials |
| `commet whoami` | Show auth status and current organization |
| `commet link` | Link project to an organization |
| `commet unlink` | Unlink project |
| `commet switch` | Switch to a different organization |
| `commet info` | Show project and auth status |
| `commet pull` | Generate `.commet/types.d.ts` |
| `commet list features` | List features |
| `commet list seats` | List seat types |
| `commet list plans` | List plans |
Configuration files [#configuration-files]
| File | Created by | Purpose |
| --------------------- | -------------- | ----------------------------- |
| `~/.commet/auth.json` | `commet login` | Global auth credentials |
| `.commet` | `commet link` | Project organization settings |
| `.commet/types.d.ts` | `commet pull` | Generated TypeScript types |
Commit `.commet/types.d.ts` for consistent types across the team.
Switch environments [#switch-environments]
Two isolated environments: **Sandbox** (`sandbox.commet.co`) and **Production** (`commet.co`).
```bash
commet logout
commet login # Select different environment
commet link
```
Update [#update]
```bash
pnpm update -g commet
```
```bash
npm update -g commet
```
```bash
yarn global upgrade commet
```
# Error Handling (/docs/error-handling)
Error Classes [#error-classes]
```typescript
import { CommetAPIError, CommetValidationError } from '@commet/node'
try {
await commet.customers.create({ email: 'invalid' })
} catch (error) {
if (error instanceof CommetValidationError) {
console.log(error.validationErrors)
// { email: ['Invalid email format'] }
}
if (error instanceof CommetAPIError) {
console.log(error.statusCode, error.message)
}
}
```
| Class | Use case |
| ----------------------- | -------------------------------- |
| `CommetAPIError` | HTTP errors (4xx, 5xx) |
| `CommetValidationError` | Invalid input with field details |
| `CommetError` | Base class for all errors |
Automatic Retries [#automatic-retries]
Failed requests retry with exponential backoff (1s → 2s → 4s, max 8s).
**Retryable:** 408, 429, 500, 502, 503, 504
```typescript
const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
retries: 3, // default
})
```
Non-Blocking Usage [#non-blocking-usage]
Don't let tracking errors break your app:
```typescript
commet.usage.track({
customerId: 'user_123',
feature: 'api_calls',
}).catch(console.error)
// Continue without waiting
```
# Introduction (/docs)
Commet is a billing and payments platform for SaaS and AI products. It handles recurring billing, taxes, compliance, and payouts so you can focus on your product.
Quickstart [#quickstart]
The Plan-First Model [#the-plan-first-model]
You define what you sell, package it into a plan, and assign it to a customer. Billing runs automatically from there.
1. **Features** — define capabilities like API calls, seats, or SSO access
2. **Plan** — bundle features with pricing and a consumption model
3. **Customer** — assign the plan and a subscription is created
4. **Billing** — invoices, usage tracking, and payments happen without intervention
Consumption Models [#consumption-models]
Every plan uses one consumption model. This defines how customers consume and pay for features.
| Model | How it works | Examples |
| ----------- | ------------------------------------------ | -------------------------- |
| **Metered** | Base price + overage billed at period end | Twilio, Resend, AWS |
| **Credits** | Prepaid credits consumed by usage | Midjourney, Cursor, Replit |
| **Balance** | Prepaid dollar balance drawn down by usage | Supabase, Railway, Vercel |
Models are mutually exclusive — each plan uses exactly one. [Learn more about consumption models](/docs/consumption-models).
Explore [#explore]
# SDK Reference (/docs/sdk-reference)
Setup [#setup]
```typescript
import { Commet } from '@commet/node'
const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
})
```
Get your API key from the [dashboard](https://commet.co/settings/api-keys).
Options [#options]
| Option | Type | Default | Description |
| ------------- | ----------------------------- | ----------- | -------------------------------- |
| `apiKey` | string | required | Your API key (starts with `ck_`) |
| `environment` | `'sandbox'` \| `'production'` | `'sandbox'` | Target environment |
| `debug` | boolean | `false` | Log requests/responses |
| `timeout` | number | `30000` | Request timeout (ms) |
| `retries` | number | `3` | Max retry attempts |
Environments [#environments]
* **Sandbox** (`sandbox.commet.co`) - Development, default
* **Production** (`commet.co`) - Live billing
```typescript
commet.isSandbox() // true
commet.isProduction() // false
```
Debug Mode [#debug-mode]
See all requests and responses:
```typescript
const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
debug: true,
})
// [Commet SDK] POST https://sandbox.commet.co/api/customers
// [Commet SDK] Response status: 200 OK
```
Pagination [#pagination]
List endpoints use cursor-based pagination.
```typescript
const page = await commet.customers.list({ limit: 25 })
if (page.hasMore) {
const nextPage = await commet.customers.list({
limit: 25,
cursor: page.nextCursor,
})
}
```
| Parameter | Type | Default | Description |
| --------- | ------ | ------- | ----------------------------- |
| `limit` | number | 25 | Items per page (max 100) |
| `cursor` | string | - | Cursor from previous response |
# Testing (/docs/testing-sandbox)
Sandbox is the default environment — completely isolated from production.
Test Flow [#test-flow]
```typescript
// 1. Create customer
const customerId = `test_${Date.now()}`
const customer = await commet.customers.create({
email: 'test@example.com',
id: customerId,
})
// 2. Create subscription
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
// 3. Pay with test card at checkoutUrl
// 4. Track usage
await commet.usage.track({
customerId,
feature: 'api_calls',
})
```
Dev Tools Panel [#dev-tools-panel]
A floating widget that appears on all sandbox pages. The [**Test Clock**](#test-clock) tab is available everywhere. The [**Test Data**](#test-data) tab (address and card presets) only appears on checkout pages.
Test Clock [#test-clock]
Simulate future dates to test billing cycles, renewals, and prorations without waiting for real time to pass.
**Current Time** — displays the simulated time in UTC, or the real time if no simulation is active.
**Set Time** — pick a future date from a calendar. Time can only move forward and cannot be reverted.
**Quick Advance** — jump forward by a preset amount:
| Button | Days |
| ---------- | ---- |
| + 1 Day | 1 |
| + 1 Week | 7 |
| + 2 Weeks | 14 |
| + 1 Month | 30 |
| + 3 Months | 90 |
**Run Billing Cron** — manually trigger billing processing at the simulated time. This processes renewals, generates invoices, and applies usage charges.
**Example workflow:** advance 1 month to reach the next billing cycle, then run billing cron to generate renewal invoices and verify charges.
Test Data [#test-data]
The Test Data tab automates checkout form filling with valid test data for 60+ countries. Open it from the Dev Tools panel on any sandbox checkout page.
**Address autofill** — select a country and the checkout address form is automatically filled with a valid name, street, city, postal code, and phone number for that country. No need to type anything manually.
**Country-specific test cards** — after selecting a country, the panel shows the test card number that matches that country. Click to copy. Expiry and CVC are always `12/34` and `123`.
**Failure scenarios** — generic cards are available to test error handling: declined payments, insufficient funds, expired cards, and 3D Secure authentication. See the tables below.
Test cards by country [#test-cards-by-country]
| Country | Card | Brand |
| ------------------------- | --------------------- | ------------ |
| United States (US) | `4242 4242 4242 4242` | Visa |
| Argentina (AR) | `4000 0032 0000 0021` | Visa |
| Australia (AU) | `4000 0003 6000 0006` | Visa |
| Austria (AT) | `4000 0004 0000 0008` | Visa |
| Belarus (BY) | `4000 0011 2000 0005` | Visa |
| Belgium (BE) | `4000 0005 6000 0004` | Visa |
| Brazil (BR) | `4000 0076 0000 0002` | Visa |
| Bulgaria (BG) | `4000 0010 0000 0000` | Visa |
| Canada (CA) | `4000 0012 4000 0000` | Visa |
| Chile (CL) | `4000 0015 2000 0001` | Visa |
| China (CN) | `4000 0015 6000 0002` | Visa |
| Colombia (CO) | `4000 0017 0000 0003` | Visa |
| Costa Rica (CR) | `4000 0018 8000 0005` | Visa |
| Croatia (HR) | `4000 0019 1000 0009` | Visa |
| Cyprus (CY) | `4000 0019 6000 0008` | Visa |
| Czech Republic (CZ) | `4000 0020 3000 0002` | Visa |
| Denmark (DK) | `4000 0020 8000 0001` | Visa |
| Ecuador (EC) | `4000 0021 8000 0000` | Visa |
| Estonia (EE) | `4000 0023 3000 0009` | Visa |
| Finland (FI) | `4000 0024 6000 0001` | Visa |
| France (FR) | `4000 0025 0000 0003` | Visa |
| Germany (DE) | `4000 0027 6000 0016` | Visa |
| Gibraltar (GI) | `4000 0029 2000 0005` | Visa |
| Greece (GR) | `4000 0030 0000 0030` | Visa |
| Hong Kong (HK) | `4000 0034 4000 0004` | Visa |
| Hungary (HU) | `4000 0034 8000 0005` | Visa |
| India (IN) | `4000 0035 6000 0008` | Visa |
| Ireland (IE) | `4000 0037 2000 0005` | Visa |
| Italy (IT) | `4000 0038 0000 0008` | Visa |
| Japan (JP) | `4000 0039 2000 0003` | Visa |
| Japan (JP) | `3530 1113 3330 0000` | JCB |
| Latvia (LV) | `4000 0042 8000 0005` | Visa |
| Liechtenstein (LI) | `4000 0043 8000 0004` | Visa |
| Lithuania (LT) | `4000 0044 0000 0000` | Visa |
| Luxembourg (LU) | `4000 0044 2000 0006` | Visa |
| Malaysia (MY) | `4000 0045 8000 0002` | Visa |
| Malta (MT) | `4000 0047 0000 0007` | Visa |
| Mexico (MX) | `4000 0048 4000 8001` | Visa |
| Mexico (MX) | `5062 2100 0000 0009` | Carnet |
| Netherlands (NL) | `4000 0052 8000 0002` | Visa |
| New Zealand (NZ) | `4000 0055 4000 0008` | Visa |
| Norway (NO) | `4000 0057 8000 0007` | Visa |
| Panama (PA) | `4000 0059 1000 0000` | Visa |
| Paraguay (PY) | `4000 0060 0000 0066` | Visa |
| Peru (PE) | `4000 0060 4000 0068` | Visa |
| Poland (PL) | `4000 0061 6000 0005` | Visa |
| Portugal (PT) | `4000 0062 0000 0007` | Visa |
| Romania (RO) | `4000 0064 2000 0001` | Visa |
| Saudi Arabia (SA) | `4000 0068 2000 0007` | Visa |
| Singapore (SG) | `4000 0070 2000 0003` | Visa |
| Slovakia (SK) | `4000 0070 3000 0001` | Visa |
| Slovenia (SI) | `4000 0070 5000 0006` | Visa |
| Spain (ES) | `4000 0072 4000 0007` | Visa |
| Sweden (SE) | `4000 0075 2000 0008` | Visa |
| Switzerland (CH) | `4000 0075 6000 0009` | Visa |
| Taiwan (TW) | `4000 0015 8000 0008` | Visa |
| Thailand (TH) | `4000 0076 4000 0003` | Visa |
| Thailand (TH) | `4000 0576 4000 0008` | Visa (debit) |
| United Arab Emirates (AE) | `4000 0078 4000 0001` | Visa |
| United Arab Emirates (AE) | `5200 0078 4000 0022` | Mastercard |
| United Kingdom (GB) | `4000 0082 6000 0000` | Visa |
| United Kingdom (GB) | `4000 0582 6000 0005` | Visa (debit) |
| United Kingdom (GB) | `5555 5582 6555 4449` | Mastercard |
| Uruguay (UY) | `4000 0085 8000 0003` | Visa |
Failure scenarios [#failure-scenarios]
| Card | Scenario |
| --------------------- | ------------------ |
| `4000 0000 0000 9995` | Insufficient funds |
| `4000 0000 0000 0002` | Card declined |
| `4000 0000 0000 0069` | Expired card |
3D Secure [#3d-secure]
| Card | Description |
| --------------------- | ----------------------- |
| `4000 0000 0000 3220` | Requires authentication |
| `4000 0025 0000 3155` | Requires authentication |
Production [#production]
```typescript
const commet = new Commet({
apiKey: process.env.COMMET_PRODUCTION_KEY!,
environment: 'production',
})
```
# Discounts (/docs/how-do-discounts-work)
Commet has two types of discounts: introductory offers (automatic, on the plan) and promo codes (manual, entered at checkout). They never stack.
Two Types of Discounts [#two-types-of-discounts]
| | Introductory Offer | Promo Code |
| ----------------- | ------------------------- | --------------------------------- |
| Who configures it | You, on the plan price | You, as a separate marketing code |
| How it's applied | Automatically at checkout | Customer enters a code |
| Who gets it | New customers only | Anyone with the code |
| Where it lives | Plan configuration | Promo Codes section in dashboard |
When Both Exist [#when-both-exist]
If a plan has an intro offer **and** the customer enters a promo code, the intro offer wins. The promo code is ignored.
This is intentional. One discount per transaction keeps billing predictable. The intro offer takes priority because it's part of the plan itself — it's the price you designed for new customers. Allowing stacking would create prices you never explicitly set.
What Happens on Plan Change [#what-happens-on-plan-change]
When a customer changes plans, any active discount is cleared. The new plan's pricing applies at full price (or with the new plan's own intro offer, if the customer qualifies).
The discount was tied to the original plan. A 50% off promo for "Pro" shouldn't carry over to "Enterprise" — that's a different product at a different price point.
How Duration Works [#how-duration-works]
Discounts apply for a set number of billing cycles, then stop automatically.
```
Customer subscribes to Pro at $99/mo with "50% off for 3 months":
Month 1: $49.50 (discounted)
Month 2: $49.50 (discounted)
Month 3: $49.50 (discounted)
Month 4: $99.00 (full price — discount expired)
```
The cycle count is based on billing periods, not calendar time. If a customer is on yearly billing with a 2-cycle discount, the discount lasts 2 years.
"Forever" Discounts [#forever-discounts]
A promo code with `forever` duration applies on every billing cycle until the customer changes plans. Changing plans clears the discount.
```
Customer uses "VIP20" (20% off forever) on Pro at $99/mo:
Month 1: $79.20
Month 2: $79.20
...
Month 24: $79.20
Customer upgrades to Enterprise → discount cleared
Month 25: $199.00 (Enterprise full price)
```
What Gets Discounted [#what-gets-discounted]
Discounts apply **only to the plan base price**. Overage charges, add-on charges, and seat overage are always billed at full price.
Example [#example]
```
Plan Pro at $100/mo with 20% intro offer.
Customer uses $50 in overage this month.
Plan base: $100.00
Discount: −$20.00 (20% of $100 base)
Overage: $50.00
Total: $130.00
```
The discount never touches the $50 overage.
Quick Reference [#quick-reference]
| Scenario | What applies |
| ------------------------------------------------------- | ------------------------------------------ |
| New customer, plan has intro offer | Intro offer |
| New customer, plan has intro offer + promo code entered | Intro offer (promo code ignored) |
| New customer, no intro offer + promo code entered | Promo code |
| Existing customer, promo code entered | Promo code |
| Customer changes plans | Discount cleared, new plan pricing applies |
| Discount duration expires | Full price from next cycle |
| "Forever" promo + plan change | Discount cleared |
| Overage, add-ons, seat overage | **Never discounted** — always full price |
Intro offers only apply to customers who haven't had a paid subscription before. Returning customers always fall through to promo code eligibility.
Related [#related]
* [Introductory Offers](/docs/introductory-offers) — Configure automatic discounts on plan prices
* [Promo Codes](/docs/promo-codes) — Create marketing discount codes
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — What happens when customers upgrade or downgrade
* [Pricing Changes](/docs/what-happens-when-you-change-your-prices) — What happens when you change your prices
# Free Plans (/docs/how-do-free-plans-work-without-payment)
A free plan has no price, no checkout, and no billing cycle. When you assign a customer to a free plan, they're activated immediately.
How Free Plans Differ from Paid Plans [#how-free-plans-differ-from-paid-plans]
| | Paid plan | Free plan |
| -------------- | ----------------------------- | ---------------------------------------------------------- |
| Price | Has a price | $0 |
| Billing | Monthly, quarterly, or yearly | None |
| How they start | Checkout + payment | **Activated immediately** |
| Invoices | Yes | Only for purchases (credits, balance, add-ons) |
| Overage | Configurable per feature | **Never allowed** — usage is blocked at the included limit |
What Your Customer Sees in the Portal [#what-your-customer-sees-in-the-portal]
The Customer Portal adapts when someone is on a free plan:
| Section | Paid plan | Free plan |
| ----------------------- | ------------------------------------------------- | -------------------------------------------- |
| Subscription | Shows plan, price, and next billing date | Shows plan and status only — no billing date |
| Invoices | Visible | Visible only if they've made purchases |
| Payment method | Visible | Visible only if they've added one |
| MRR | Shows amount | Shows $0.00/mo |
| Usage (balance/credits) | Visible, with "Add Funds" or "Buy Credits" button | Visible, with purchase buttons |
Purchasing on a Free Plan [#purchasing-on-a-free-plan]
Free plan customers can purchase add-ons, credit packs, and balance top-ups — the same one-off purchases available on paid plans. Since they didn't go through checkout, they don't have a card on file. The first purchase prompts them to enter a payment method, which is saved for future purchases.
The plan itself is free. Purchases are optional extras that customers choose to buy.
Changing a Free Plan's Included Balance or Credits [#changing-a-free-plans-included-balance-or-credits]
If you change the included balance or credits on a free plan, the change reaches **all customers on that plan right away**.
Example [#example]
```
You increase the free plan's included balance from $100 to $150.
Every customer on that plan immediately gets +$50 added to their balance.
```
This follows the core principle — giving your customers more resources benefits them, so it applies immediately.
When Your Customer Upgrades to a Paid Plan [#when-your-customer-upgrades-to-a-paid-plan]
The upgrade is always **immediate**. Since the free plan costs $0, there's no credit to give — your customer simply pays the full price of the new plan from that day.
Example [#example-1]
```
Your customer is on a Free plan with $100 included balance.
They upgrade to Pro at $99/mo.
Credit from free plan: $0 (it's free)
They pay: $99 (full month)
Invoices, payment method, and billing info appear in their portal.
```
Related [#related]
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — Upgrades, downgrades, and switching
* [Trials](/docs/how-do-trial-periods-work) — Another way to let customers try before paying
* [Invoices](/docs/what-invoices-do-customers-receive-and-when) — What invoices your customers receive
# Billing Intervals (/docs/how-do-monthly-quarterly-and-yearly-billing-work)
Plans can be billed monthly, quarterly, or yearly. Even on quarterly and yearly plans, Commet checks for usage charges **every month**. Here's what your customers can expect.
Monthly Plans [#monthly-plans]
Straightforward: your customer is billed every month for their plan base, extra usage, and extra seats.
Quarterly and Yearly Plans [#quarterly-and-yearly-plans]
Your customer pays the plan base every 3 or 12 months, but **usage charges can happen every month**. Here's how it works:
Example: Quarterly plan at $300/quarter [#example-quarterly-plan-at-300quarter]
```
Month 1:
→ Your customer used extra API calls? They get a small invoice for just the overage.
→ No extra usage? No invoice at all.
Month 2:
→ Same thing — only charged if they had extra usage.
Month 3 (renewal month):
→ Full invoice: plan base ($300) + any extra usage + any extra seats.
→ Billing cycle resets for the next quarter.
```
Usage resets **every month**, not every quarter or year. A customer on a quarterly plan with 10,000 included API calls gets 10,000 calls each month — they don't accumulate.
What Your Customer Sees [#what-your-customer-sees]
| | Months between renewals | Renewal month |
| --------------------- | ----------------------- | ----------------------- |
| Plan base | Not charged | Charged |
| Extra usage | Charged (if any) | Charged |
| Extra seats | Not charged | Charged |
| No extra usage at all | No invoice | Invoice (has plan base) |
Plan Changes Mid-Cycle [#plan-changes-mid-cycle]
If your customer upgrades a quarterly or yearly plan mid-cycle, they get credit for the unused portion and start a new cycle from the change date. See [Proration](/docs/how-is-proration-calculated-when-changing-plans) for the exact calculation.
Example [#example]
```
Your customer is on Plan A at $300/quarter (January 1 – April 1).
They upgrade to Plan B at $600/quarter on February 15.
→ They get credit for the unused portion of Plan A.
→ They're charged a prorated amount for Plan B.
→ Their new cycle starts February 15, next renewal May 15.
```
Related [#related]
* [Invoices](/docs/what-invoices-do-customers-receive-and-when) — What invoices your customers receive and when
* [Proration](/docs/how-is-proration-calculated-when-changing-plans) — How mid-cycle charges are calculated
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — Upgrades, downgrades, and switching
# Trials (/docs/how-do-trial-periods-work)
You can offer a trial period on any paid plan. During the trial, your customer uses the product without being charged. They provide their card upfront, and Commet charges them automatically when the trial ends.
What Your Customer Experiences [#what-your-customer-experiences]
| | Regular checkout | Trial checkout |
| --------------- | --------------------------- | ------------------------------ |
| What happens | They're charged immediately | Their card is saved, no charge |
| Button they see | "Pay" | "Start Free Trial" |
| After checkout | They see the amount charged | They see a trial confirmation |
How It Works [#how-it-works]
```
1. You create a subscription with a trial-enabled plan
→ Your customer receives an email with a checkout link
2. They open checkout and enter their card
→ No charge — their card is just saved for later
→ Their trial begins
3. Trial period ends
→ They're automatically charged the current plan price
→ Their subscription becomes active
```
If your customer's card requires 3D Secure verification, they complete it during checkout. The trial starts once verification succeeds. The experience is the same — just one extra step.
What Happens if You Change the Price During a Trial [#what-happens-if-you-change-the-price-during-a-trial]
Your customer pays the **price at the time the trial ends**, not the price when they started.
Example [#example]
```
Your customer starts a 14-day trial on January 1.
On day 10, you raise the price from $99/mo to $129/mo.
On day 15, the trial ends.
Your customer is charged $129/mo.
```
A trial is a test period, not a price commitment. Your customer hasn't paid anything yet, so the current price applies when billing begins.
During the Trial [#during-the-trial]
Your customer gets the plan's features and included limits, with a few differences from an active subscription:
| Behavior | Active subscription | During trial |
| -------------------------------- | --------------------- | -------------------------------------------- |
| Overage | Charged at period end | **Blocked** — usage stops at included limits |
| Buy credits, balance, or add-ons | Yes | **Yes** — card was captured at checkout |
| Limits reset (trials > 30 days) | Every billing cycle | **Every 30 days** |
Your customer can use the product normally and even make purchases, but they won't accumulate surprise overage charges during a trial.
Related [#related]
* [Free Plans](/docs/how-do-free-plans-work-without-payment) — An alternative way to let customers try your product
* [Payment Failures](/docs/what-happens-when-a-payment-fails) — What happens if the first charge fails
* [Invoices](/docs/what-invoices-do-customers-receive-and-when) — What invoices your customers receive
# How Billing Works (/docs/how-does-billing-work)
**Commet does the right thing by default.** Every billing decision follows one rule:
> **If a change benefits your customer, it applies immediately. If it harms them, it applies at renewal.**
No configuration needed. This is how every scenario works.
Quick Reference [#quick-reference]
When You Make Changes [#when-you-make-changes]
| What you do | What happens to existing customers |
| --------------------------------------------- | ---------------------------------------------------- |
| Raise the price | They keep their current price until renewal |
| Lower the price | They keep their current price until renewal |
| Change usage-based pricing | New price applies starting next period |
| Increase limits or included seats | **They get it right away** |
| Decrease limits or included seats | Keeps current limits until renewal |
| Add a feature to a plan | **They get it right away** |
| Remove a feature from a plan | Keeps the feature until renewal |
| Deprecate a plan | They keep their plan, nothing changes |
| Delete a plan | You choose: cancel them or migrate them (at renewal) |
| Delete a customer | Their subscription ends immediately |
| Change a free plan's included balance/credits | **Updates right away** for everyone on the plan |
When Your Customer Makes Changes [#when-your-customer-makes-changes]
| What they do | What happens |
| -------------------------------- | ---------------------------------------------------------------------- |
| Upgrade to a more expensive plan | **Immediate** — they get credit for unused days and pay the difference |
| Downgrade to a cheaper plan | Keeps current plan until renewal, then switches |
| Switch from free to paid | **Immediate** — pays full price, no credit (free = $0) |
| Switch from monthly to yearly | **Immediate** |
| Switch from yearly to monthly | Keeps yearly plan until it expires, then switches |
Automatic Events [#automatic-events]
| What happens | What your customer experiences |
| --------------------- | ---------------------------------------------------------------------------------------- |
| Trial starts | They use the product, no charge. **Overage is blocked** — usage stops at included limits |
| Trial ends | They're charged the current price, overage activates normally |
| Payment fails | They keep access while we retry — canceled only if all retries fail |
| Canceled subscription | They'd need to start a new subscription at the current price |
Decision Tree [#decision-tree]
```
Who makes the change?
│
├── YOU (Dashboard)
│ │
│ ├── Does it benefit your customer?
│ │ ├── YES (more limits, new feature) → IMMEDIATE
│ │ └── NO (fewer limits, higher price) → AT RENEWAL
│ │
│ ├── Free plan?
│ │ └── Activated instantly, no checkout. Can still buy credits/balance/add-ons
│ │
│ ├── Trial plan?
│ │ └── Card captured, tries product (overage blocked), can buy extras, charged when trial ends
│ │
│ ├── Deleting a plan?
│ │ └── Choose: cancel or migrate to another plan → AT RENEWAL
│ │
│ └── Deleting a customer?
│ └── Subscription ends → IMMEDIATE
│
└── YOUR CUSTOMER (Portal)
│
├── Upgrading?
│ └── IMMEDIATE — pays the difference for remaining days
│
├── Going from free to paid?
│ └── IMMEDIATE — pays full price
│
├── Downgrading?
│ └── AT RENEWAL — enjoys current plan until it expires
│
└── New plan has lower limits than their current usage?
└── Allowed, but they'll see a warning about extra costs
```
Explore Each Topic [#explore-each-topic]
* [Pricing Changes](/docs/what-happens-when-you-change-your-prices) — What happens when you change your prices
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — Upgrades, downgrades, and switching plans
* [Billing Intervals](/docs/how-do-monthly-quarterly-and-yearly-billing-work) — Monthly, quarterly, and yearly billing
* [Invoices](/docs/what-invoices-do-customers-receive-and-when) — What invoices your customers receive and when
* [Proration](/docs/how-is-proration-calculated-when-changing-plans) — How mid-cycle charges are calculated
* [Trials](/docs/how-do-trial-periods-work) — How trial periods work
* [Free Plans](/docs/how-do-free-plans-work-without-payment) — How free plans work
* [Payment Failures](/docs/what-happens-when-a-payment-fails) — What happens when a payment fails
* [Seats](/docs/how-does-seat-based-billing-work) — How seat-based billing works
# Seats (/docs/how-does-seat-based-billing-work)
Plans can include a number of seats at no extra cost. When your customer uses more seats than included, they're charged per extra seat.
How It Works [#how-it-works]
```
Plan Pro: 5 included seats, $25/extra seat
Your customer has 8 seats
Included: 5 seats (no charge)
Extra: 3 seats × $25 = $75/mo
```
What Happens When You Change Included Seats [#what-happens-when-you-change-included-seats]
Adding more included seats [#adding-more-included-seats]
More included seats **benefits your customers**, so it applies right away.
```
You increase Plan Pro from 10 to 15 included seats.
Your customer has 12 seats (was paying for 2 extra at $50/mo).
After the change:
→ All 12 seats are now within the 15 included
→ Extra seat charges drop to $0
→ Your customer sees the change immediately
```
Reducing included seats [#reducing-included-seats]
Fewer included seats harms your customers, so it applies **at renewal**.
```
You decrease Plan Pro from 15 to 10 included seats.
Your customer has 12 seats (all included, $0 extra).
This period: Still 12 included, $0 extra
At renewal: 10 included + 2 extra = $50/mo
```
What Happens When You Change Seat Prices [#what-happens-when-you-change-seat-prices]
Same as all price changes: your customers keep the current price until renewal. The new price applies starting next period.
| | What your customer pays |
| ----------- | ----------------------- |
| This period | Old price per seat |
| Next period | New price per seat |
Seats and Upgrades [#seats-and-upgrades]
When your customer upgrades to a plan with more included seats, their extra seat charges may decrease or disappear.
Example [#example]
```
Your customer is on Pro ($99/mo, 5 included seats, $25/extra seat).
They have 8 seats — paying $174/mo total (5 included + 3 extra).
They upgrade to Business ($299/mo, 10 included seats, $20/extra seat).
On day 15 of their cycle.
Credit:
Plan base: $99 × (15/30) = $49.50
Extra seats: $75 × (15/30) = $37.50
Total credit: $87.00
Charge:
New plan: $299 × (15/30) = $149.50
Extra seats: $0 — their 8 seats are now within 10 included
Total charge: $149.50
They pay today: $62.50
```
Your customer's 8 seats are now covered by the Business plan's 10 included seats. They stop paying for extra seats entirely.
Related [#related]
* [Proration](/docs/how-is-proration-calculated-when-changing-plans) — How mid-cycle charges are calculated
* [Pricing Changes](/docs/what-happens-when-you-change-your-prices) — How price changes apply to existing customers
* [Billing Intervals](/docs/how-do-monthly-quarterly-and-yearly-billing-work) — When seats are charged on quarterly and yearly plans
# Proration (/docs/how-is-proration-calculated-when-changing-plans)
When your customer upgrades mid-cycle, they get credit for the days they already paid for on the old plan, and are charged for the remaining days on the new plan. Here's how the math works.
The Calculation [#the-calculation]
```
Credit = Old price × (days remaining / days in cycle)
Charge = New price × (days remaining / days in cycle)
They pay = Charge - Credit
```
Example: Simple Upgrade [#example-simple-upgrade]
```
Your customer is on Starter at $29/mo, paid on January 1.
They upgrade to Pro at $99/mo on January 15 (15 days remaining).
Credit for unused Starter days: $29 × (15/30) = $14.50
Charge for remaining Pro days: $99 × (15/30) = $49.50
They pay today: $35.00
Next full invoice: $99 on February 15
```
Example: Upgrade with Extra Seats [#example-upgrade-with-extra-seats]
When your customer has extra seats, those are prorated too.
```
Current plan: Pro $99/mo, 5 included seats, $25/extra seat
Your customer has 8 seats (5 included + 3 extra = $174/mo total)
They upgrade to: Business $299/mo, 10 included seats, $20/extra seat
On January 15 (15 days remaining)
```
| | Calculation |
| -------------------------- | ------------------------------------------------- |
| Credit for plan base | $99 × (15/30) = $49.50 |
| Credit for extra seats | $75 × (15/30) = $37.50 |
| **Total credit** | **$87.00** |
| Charge for new plan base | $299 × (15/30) = $149.50 |
| Charge for new extra seats | $0 — their 8 seats are now within the 10 included |
| **Total charge** | **$149.50** |
| **They pay today** | **$62.50** |
Notice that your customer's 8 seats are now fully covered by the Business plan's 10 included seats, so they stop paying for extra seats entirely.
Quarterly and Yearly Plans [#quarterly-and-yearly-plans]
The same calculation applies — the only difference is the cycle length.
Example [#example]
```
Your customer is on Plan A at $300/quarter (January 1 – April 1).
They upgrade to Plan B at $600/quarter on February 15 (45 days remaining).
Credit: $300 × (45/90) = $150.00
Charge: $600 × (45/90) = $300.00
They pay today: $150.00
Next renewal: May 15
```
Why Downgrades Aren't Prorated [#why-downgrades-arent-prorated]
Downgrades take effect **at renewal**. Your customer already paid for the current cycle and keeps their plan until it expires. Since there's no mid-cycle switch, there's nothing to prorate and no refund.
Why Free → Paid Isn't Prorated [#why-free--paid-isnt-prorated]
When your customer moves from a free plan to a paid plan, there's no credit to give — the free plan costs $0. They simply pay the full price of the new plan from that day.
Related [#related]
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — When proration applies and when it doesn't
* [Seats](/docs/how-does-seat-based-billing-work) — How seat-based billing works with upgrades
* [Invoices](/docs/what-invoices-do-customers-receive-and-when) — What invoices your customers receive
# Plan Changes (/docs/what-happens-when-a-customer-changes-plans)
Your customers can switch plans through the [Customer Portal](/docs/customer-portal). What happens depends on whether the new plan costs more or less.
Upgrades [#upgrades]
When your customer moves to a **more expensive plan**, the change is **immediate**. They get credit for the unused days on their current plan and are charged the prorated price of the new plan.
Example [#example]
```
Your customer is on Starter at $29/mo, paid on January 1.
They upgrade to Pro at $99/mo on January 15 (15 days left).
Credit for unused Starter days: $29 × (15/30) = $14.50
Charge for remaining Pro days: $99 × (15/30) = $49.50
They pay today: $35.00
Next full invoice: $99 on February 15
```
Downgrades [#downgrades]
When your customer moves to a **cheaper plan**, the change happens **at renewal**. They keep their current plan and features until the end of the period they already paid for, then switch.
Your customer already paid for this period. They keep full access until it expires — no partial refunds, no disruption.
Free to Paid [#free-to-paid]
When your customer moves from a free plan to a paid plan, the change is always **immediate**. There's no credit to issue since the free plan costs $0, so they pay the full price of the new plan.
Example [#example-1]
```
Your customer is on a Free plan with $100 included balance.
They upgrade to Pro at $99/mo.
Credit from free plan: $0 (it's free)
They pay today: $99 (full month)
```
Changing Billing Frequency [#changing-billing-frequency]
| Change | What happens |
| ---------------- | ------------------------------------------------------------------------ |
| Monthly → Yearly | **Immediate** — this is essentially an upgrade (better price commitment) |
| Yearly → Monthly | At renewal — this is essentially a downgrade |
Deprecating a Plan [#deprecating-a-plan]
When you deprecate a plan, it disappears from your pricing page, dashboard, and portal — but **your existing customers keep it**. Their billing continues normally. If they cancel, they won't be able to come back to that plan.
Deleting a Plan [#deleting-a-plan]
When you delete a plan, you choose what happens to existing customers:
**Option A — Cancel them:** Their subscriptions end at the end of their current period. They keep access until then, but won't be billed again.
**Option B — Migrate them:** They keep their current plan until renewal, then automatically switch to the plan you choose. They'll be charged the new plan's price starting at renewal.
Both options take effect **at renewal**, not immediately.
Reactivation [#reactivation]
There's no "reactivate" button. Once a subscription is canceled, it's done. If your customer wants to come back, they start a new subscription at the current price — like any new customer. If you deprecated the plan, they'll need to pick a different one.
Feature Changes [#feature-changes]
When you change the features on a plan, the benefit/harm rule applies:
| What you do | Existing customers |
| --------------------------------- | -------------------------------- |
| Add more limits or a new feature | **Get it right away** |
| Reduce limits or remove a feature | Keep current setup until renewal |
Example [#example-2]
```
You lower Plan Pro from 10,000 API calls to 5,000.
New customers get 5,000.
Existing customers keep 10,000 until renewal, then switch to 5,000.
```
```
You raise Plan Pro from 5,000 API calls to 10,000.
All customers — new and existing — get 10,000 right away.
```
Related [#related]
* [Proration](/docs/how-is-proration-calculated-when-changing-plans) — Exactly how mid-cycle charges are calculated
* [Pricing Changes](/docs/what-happens-when-you-change-your-prices) — What happens when you change prices without changing plans
* [Billing Intervals](/docs/how-do-monthly-quarterly-and-yearly-billing-work) — How quarterly and yearly billing works
# Payment Failures (/docs/what-happens-when-a-payment-fails)
When a payment fails, your customer keeps access to your product while Commet retries the charge. If all retries fail, their subscription is canceled.
What Your Customer Experiences [#what-your-customer-experiences]
```
Payment fails
→ They keep access (grace period begins)
→ Commet retries the charge automatically
All retries fail
→ Their subscription is canceled
→ Access is revoked
```
| Stage | Access | What's happening |
| --------------------- | ---------------- | ----------------------------------------- |
| Payment fails | **Still active** | Retries are scheduled |
| During grace period | **Still active** | Automatic retries in progress |
| All retries exhausted | Revoked | Subscription canceled, no further charges |
Your customer keeps full access during the grace period. This gives them time to update their payment method without any service interruption.
Deleting a Customer [#deleting-a-customer]
When you delete a customer from the dashboard, their subscription is canceled immediately. They lose access right away, and any pending charges are not collected.
Related [#related]
* [Invoices](/docs/what-invoices-do-customers-receive-and-when) — What invoices your customers receive
* [Trials](/docs/how-do-trial-periods-work) — What happens if the first charge after a trial fails
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — How a canceled customer can come back
# Pricing Changes (/docs/what-happens-when-you-change-your-prices)
When you change a price, **new customers pay the new price immediately**. Existing customers keep the price they're currently paying and switch to the new price at their next renewal.
This applies whether you raise or lower the price — the rule is the same either way.
Base Price [#base-price]
| | New customers | Existing customers |
| ------------------- | ------------- | -------------------------------- |
| You raise the price | Pay new price | Keep current price until renewal |
| You lower the price | Pay new price | Keep current price until renewal |
Example [#example]
```
You change Plan Pro from $99/mo to $129/mo.
A new customer signs up today → pays $129/mo.
An existing customer on day 15 of their month → keeps paying $99/mo.
That same customer at renewal → starts paying $129/mo.
```
This means you can change prices without worrying about disrupting current customers. They'll always finish their current period at the price they paid.
Usage-Based Pricing (Overage) [#usage-based-pricing-overage]
When you change the per-unit price for extra usage, the same rule applies: your customer is always billed at the price that was set when their current period started.
Example [#example-1]
```
You change the overage price from $0.002/call to $0.005/call.
Your customer has already used 5,000 extra calls this month.
This month's invoice → charged at $0.002 (the price when the period started)
Next month's invoice → charged at $0.005 (the new price)
```
Seat Pricing [#seat-pricing]
Seat prices work the same way as usage pricing. If you change the per-seat price, your customers keep the old price for the rest of their current period.
Example [#example-2]
```
You change the extra seat price from $25/seat to $35/seat.
Your customer has 3 extra seats.
This month → 3 × $25 = $75
Next month → 3 × $35 = $105
```
Related [#related]
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — What happens when your customer upgrades or downgrades
* [Proration](/docs/how-is-proration-calculated-when-changing-plans) — How mid-cycle charges are calculated
* [Seats](/docs/how-does-seat-based-billing-work) — How seat-based billing works
# Invoices (/docs/what-invoices-do-customers-receive-and-when)
Your customers receive invoices automatically based on what happens in their subscription. Here's every type of invoice they might see and when it's generated.
Types of Invoices [#types-of-invoices]
| Invoice | When your customer receives it |
| --------------- | ---------------------------------------------------------------------------- |
| Recurring | Every billing cycle — includes plan base, extra usage, and extra seats |
| Overage | Between billing cycles (quarterly/yearly plans only) if they had extra usage |
| Plan change | When they upgrade or switch plans mid-cycle |
| Credit purchase | When they buy a credit pack |
| Balance top-up | When they add funds to their balance |
| Adjustment | When you manually issue a correction or one-off charge |
Recurring [#recurring]
This is the standard invoice your customer receives on each billing cycle. It includes everything: plan base price, extra usage charges, and extra seat charges.
Example [#example]
```
Monthly invoice for a customer on Plan Pro at $99/mo:
Plan base: $99.00
Extra usage (2,000 API calls × $0.01): $20.00
Extra seats (2 × $25): $50.00
─────────────────────────────────────────────
Total: $169.00
```
Overage [#overage]
For customers on quarterly or yearly plans, if they go over their included usage **between billing cycles**, they receive a smaller invoice covering just the extra usage. No plan base or seat charges — just the overage.
Example [#example-1]
```
A customer on a quarterly plan used 500 extra API calls in month 2:
Extra usage (500 × $0.01): $5.00
─────────────────────────────────
Total: $5.00
```
If they had no extra usage that month, they don't receive any invoice.
Plan Change [#plan-change]
When your customer upgrades through the portal, they receive an invoice showing the credit for unused days on the old plan and the charge for the new plan.
Example [#example-2]
```
Customer upgrades from Starter ($29/mo) to Pro ($99/mo) on day 15:
Credit for unused Starter days: -$14.50
Charge for remaining Pro days: $49.50
────────────────────────────────────────
Total: $35.00
```
Credit Purchase, Balance Top-Up, and Adjustment [#credit-purchase-balance-top-up-and-adjustment]
* **Credit purchase** — when your customer buys a credit pack through the portal or checkout.
* **Balance top-up** — when your customer adds funds to their balance.
* **Adjustment** — when you manually create a correction, refund, or one-off charge from the dashboard.
Related [#related]
* [Billing Intervals](/docs/how-do-monthly-quarterly-and-yearly-billing-work) — When quarterly and yearly customers get charged
* [Proration](/docs/how-is-proration-calculated-when-changing-plans) — How mid-cycle charges are calculated
* [Plan Changes](/docs/what-happens-when-a-customer-changes-plans) — When plan change invoices are created
# Introduction (/docs/webhooks/introduction)
Webhooks let your application receive real-time HTTP notifications when events happen in Commet — like a subscription being activated, a payment failing, or an invoice being created.
How it works [#how-it-works]
1. You register an endpoint URL in the Commet dashboard
2. You select which events you want to receive
3. When an event occurs, Commet sends a `POST` request to your URL with the event data
Payload structure [#payload-structure]
Every webhook delivers a JSON payload with this envelope:
```json
{
"event": "subscription.activated",
"timestamp": "2026-03-25T14:30:00.000Z",
"organizationId": "org_abc123",
"data": {
// Event-specific fields
}
}
```
| Field | Type | Description |
| ---------------- | ------ | ------------------------------------------------------------------- |
| `event` | string | The event type (e.g. `subscription.activated`) |
| `timestamp` | string | ISO 8601 datetime when the event was emitted |
| `organizationId` | string | Your organization ID |
| `data` | object | Event-specific payload — see [Webhook Events](/docs/webhook-events) |
Handling webhooks with Next.js [#handling-webhooks-with-nextjs]
The `@commet/next` package provides a handler that verifies signatures and routes events automatically.
```ts title="app/api/webhooks/commet/route.ts"
import { Webhooks } from "@commet/next";
export const POST = Webhooks({
webhookSecret: process.env.COMMET_WEBHOOK_SECRET!,
onSubscriptionActivated: async (payload) => {
// Grant access to your product
await db.update(users)
.set({ isPaid: true })
.where(eq(users.id, payload.data.customerId));
},
onSubscriptionCanceled: async (payload) => {
// Revoke access
await db.update(users)
.set({ isPaid: false })
.where(eq(users.id, payload.data.customerId));
},
// Catch-all for events without a specific handler
onPayload: async (payload) => {
console.log(`Received: ${payload.event}`);
},
});
```
Verifying signatures manually [#verifying-signatures-manually]
If you're not using `@commet/next`, verify the HMAC-SHA256 signature yourself using `@commet/node`:
```ts
import { Commet } from "@commet/node";
const commet = new Commet({ apiKey: process.env.COMMET_API_KEY! });
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("x-commet-signature");
const payload = commet.webhooks.verifyAndParse({
rawBody,
signature,
secret: process.env.COMMET_WEBHOOK_SECRET!,
});
if (!payload) {
return new Response("Invalid signature", { status: 403 });
}
// Handle the event
switch (payload.event) {
case "subscription.activated":
// Grant access
break;
case "subscription.canceled":
// Revoke access
break;
}
return new Response("OK", { status: 200 });
}
```
Headers [#headers]
Commet sends these headers with every webhook request:
| Header | Description |
| -------------------- | ---------------------------------------------- |
| `X-Commet-Signature` | HMAC-SHA256 hex signature of the raw body |
| `X-Commet-Event` | The event type (e.g. `subscription.activated`) |
| `X-Commet-Timestamp` | ISO 8601 datetime when the event was emitted |
| `Content-Type` | `application/json` |
Retry policy [#retry-policy]
If your endpoint returns a non-2xx status or times out (10 seconds), Commet retries with exponential backoff:
| Attempt | Delay |
| --------- | ---------- |
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 30 minutes |
| 5th retry | 1 hour |
| 6th retry | 2 hours |
| 7th retry | 4 hours |
| 8th retry | 6 hours |
After 8 failed attempts, the delivery is marked as failed. You can monitor delivery status in the Commet dashboard.
Related [#related]
* [Webhook Events](/docs/webhook-events) — all events and their payloads
# invoice.created (/docs/webhooks/invoice-created)
# payment.failed (/docs/webhooks/payment-failed)
# payment.received (/docs/webhooks/payment-received)
# subscription.activated (/docs/webhooks/subscription-activated)
# subscription.canceled (/docs/webhooks/subscription-canceled)
# subscription.created (/docs/webhooks/subscription-created)
# subscription.plan_changed (/docs/webhooks/subscription-plan-changed)
# subscription.updated (/docs/webhooks/subscription-updated)
# Customer Portal (/docs/customer-portal)
Self-service page where customers manage their subscription, view usage, change plans, and purchase credits. Automatically created with every subscription.
{/* TODO: Add screenshot of Customer Portal */}
What customers can do [#what-customers-can-do]
* View current subscription and billing cycle
* See usage across all features
* Upgrade or downgrade plans (requires a [Plan Group](/docs/plan-groups))
* Purchase [Credit Packs](/docs/credit-packs), balance top-ups, and [Add-ons](/docs/add-ons)
* Update payment method and billing details
* View invoice history
* Cancel subscription
Purchases are available on active, trialing, and free plan subscriptions. Free plan customers are prompted to add a payment method on their first purchase.
Generate a portal URL [#generate-a-portal-url]
Portal URLs are single-use and expire after **2 hours** of inactivity. Generate a fresh URL on each request.
```typescript
const portal = await commet.portal.getUrl({ customerId: 'user_123' })
redirect(portal.data.portalUrl)
```
You can identify customers by `customerId` (Commet ID or your external ID) or `email`.
Next.js helper [#nextjs-helper]
`@commet/next` provides a one-line route handler that generates portal URLs and redirects automatically.
```typescript title="app/api/commet/portal/route.ts"
import { CustomerPortal } from '@commet/next'
export const GET = CustomerPortal({
apiKey: process.env.COMMET_API_KEY!,
getCustomerId: async (req) => {
return 'user_123'
},
})
```
```tsx
Manage Billing
```
Related [#related]
* [Manage Subscriptions](/docs/manage-subscriptions)
* [Plan Groups](/docs/plan-groups)
* [Credit Packs](/docs/credit-packs)
# Manage Customers (/docs/manage-customers)
A customer represents the business or person you bill. Each customer can have one active subscription at a time.
Dashboard [#dashboard]
Navigate to **Customers** to view, search, and manage customers. From a customer's detail page you can assign plans, view subscription details, and manage billing.
{/* TODO: Add screenshot of Customers list in dashboard */}
Create a customer [#create-a-customer]
```typescript
const { data } = await commet.customers.create({
email: 'billing@acme.com',
id: 'user_123', // optional — your user ID for easy lookup
})
// data.id → 'cus_abc123' (Commet ID)
```
`create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
Parameters [#parameters]
| Parameter | Type | Required | Description |
| ---------- | -------- | -------- | ---------------------------------------------------------- |
| `email` | `string` | Yes | Billing email |
| `id` | `string` | No | Your user ID for easy lookup |
| `fullName` | `string` | No | Customer name |
| `address` | `object` | No | Billing address (`line1`, `city`, `postalCode`, `country`) |
| `metadata` | `object` | No | Custom key-value pairs |
Get a customer [#get-a-customer]
```typescript
const { data } = await commet.customers.get('cus_abc123')
```
Update a customer [#update-a-customer]
```typescript
await commet.customers.update({
customerId: 'cus_abc123',
email: 'new@acme.com',
address: { line1: '123 Main St', city: 'Austin', postalCode: '78701', country: 'US' },
})
```
Parameters [#parameters-1]
| Parameter | Type | Required | Description |
| ------------ | -------- | -------- | ---------------------------------------------------------------------------- |
| `customerId` | `string` | Yes | Commet ID (`cus_xxx`) or your user ID |
| `email` | `string` | No | New billing email |
| `fullName` | `string` | No | Customer name |
| `timezone` | `string` | No | IANA timezone |
| `metadata` | `object` | No | Custom key-value pairs |
| `address` | `object` | No | Billing address (`line1`, `line2`, `city`, `state`, `postalCode`, `country`) |
List customers [#list-customers]
Cursor-based pagination. Returns up to 100 customers per page.
```typescript
const { data, hasMore, nextCursor } = await commet.customers.list({ limit: 25 })
```
To fetch the next page, pass the `nextCursor` value.
```typescript
const nextPage = await commet.customers.list({
limit: 25,
cursor: nextCursor,
})
```
Archive a customer [#archive-a-customer]
```typescript
await commet.customers.archive('cus_abc123')
```
Archived customers cannot be reactivated.
Related [#related]
* [Customer Portal](/docs/customer-portal)
* [Manage Subscriptions](/docs/manage-subscriptions)
# Acceptable Use Policy (/docs/acceptable-use)
Commet is the Merchant of Record for your digital products, meaning we operate as the official reseller. Our platform is designed for digital offerings only and does not accommodate physical items or human-delivered services.
**Have questions about your specific use case?** [Contact our support team](mailto:help@commet.co) before starting your integration.
What you can sell [#what-you-can-sell]
Commet enables you to monetize these digital product and service categories:
* **Premium access**: Subscription-based content, gated digital experiences, private GitHub repositories, Discord servers, and online courses
* **Digital products**: Code repositories, templates, eBooks, PDFs, icons, fonts, design resources, photos, videos, and audio files
* **Software & SaaS**: Mobile applications, desktop software, web applications, and software-as-a-service offerings
Core requirements [#core-requirements]
For your product to be eligible:
1. **Compliant** — Lawful, ethical, transparent, and consistent with payment provider regulations
2. **High quality** — Something you'd confidently present publicly
3. **Digitally fulfillable** — Deliverable automatically through Commet or instantly available via your service using our APIs
Restricted categories [#restricted-categories]
Certain business types need extra scrutiny and must demonstrate elevated standards:
* **eBooks**: Must deliver real value and meet quality benchmarks
* **Ticket sales**: Must comply with regional regulations
* **Pre-orders & paid waitlists**: Higher risk for MoR operations; exceptions possible for established developers with proven delivery records
* **Marketing services**: Must emphasize authentic, sustainable value
* **Directories & boards**: Frequently feature paid positioning that may lack appropriate disclosure
Prohibited products & services [#prohibited-products--services]
**This list is not comprehensive.** We may introduce new restrictions at any point. Accounts could be subject to review or suspension if we identify misleading, fraudulent, or high-risk usage patterns.
Compliance violations [#compliance-violations]
* Services that breach terms of service on other platforms
* Counterfeit products
* Items you don't have ownership rights to or lack licensing to resell
* Intellectual property or trademark violations
Restricted business models [#restricted-business-models]
* Travel-related services: reservations, travel clubs, timeshares
* Recruitment platforms or job boards
* Paid waitlists or pre-orders (exceptions possible for high-trust scenarios)
* Charitable donations or charity where pricing significantly exceeds product value
Marketplaces & reselling [#marketplaces--reselling]
* Platforms that enable unauthorized resale or distribution
* Unauthorized software license reselling
* Marketplaces that sell products or services from other parties
Financial services [#financial-services]
* NFT and cryptocurrency assets
* Financial guidance: investment strategies, trading signals, wealth management, tax advice
* Investment advisory, brokerage, or trading services
* Financial services: account balances, investments, or transaction facilitation
Prohibited service types [#prohibited-service-types]
* Services designed to bypass rules or terms of other services
* Cheating services: unauthorized modifications, hacks, or unfair advantages
* IP or API cloaking services
* eSIM or telecommunication services
* Malware, spyware, or viruses
* IPTV services
* Products that need special licensing or are heavily regulated
* Wagering, betting, or gambling services
Marketing & outreach services [#marketing--outreach-services]
* Unsolicited marketing or advertising services, including:
* Automation for mass content generation and submission
* Automated outreach that risks spam
* Bulk messaging via SMS/WhatsApp
* Lead selling, scraping, or generation
Other prohibited categories [#other-prohibited-categories]
* Services that could damage the reputation of Commet or our payment processing partners
* "Get rich quick" content or schemes
* Medical services: pharmaceutical products, weight loss, or medical advice
* Pseudo-science offerings: fortune-telling, horoscopes, clairvoyance
Low-quality products [#low-quality-products]
* Deceptive practices: fake social proof, reviews, or testimonials
* Products with low trustworthiness scores or misleading tactics
* Services or products with major bugs or execution problems
* Premium-priced AI-generated content (e.g., $50 eBooks that are only 4 pages)
Adult content & services [#adult-content--services]
* Explicit or adult content, including NSFW content created through AI, services similar to OnlyFans, and adult services or content generated with AI
Data & privacy violations [#data--privacy-violations]
* Services that undermine user privacy or data security
* Unauthorized distribution or resale of customer data to third parties
Physical goods & human services [#physical-goods--human-services]
* Services that aren't digitally or automatically fulfillable
* Professional services: web development, design, marketing, consulting
* Services that need physical delivery or fulfillment
* Physical items of any type
Illegal & restricted content [#illegal--restricted-content]
* Content that conflicts with payment processor guidelines (e.g., [Stripe's restricted businesses](https://stripe.com/en-se/legal/restricted-businesses))
* Products that break laws in your operating jurisdiction or target markets
* Services or content aimed at minors or that are age-restricted
* Illegal products or services, such as vaping products, tobacco, alcohol, or drugs
Related [#related]
* [Merchant of Record](/docs/merchant-of-record) — How Commet handles taxes and compliance as your MoR
* [Finance Overview](/docs/finance-overview) — Balances, payouts, and transaction history
# Finance Overview (/docs/finance-overview)
The finance module tracks how money flows from customer payments to your bank account — balance tracking, payouts, and a complete transaction history.
{/* TODO: Add screenshot */}
Balance states [#balance-states]
Your money moves through four states as it's processed.
| State | Description | Example |
| ------------- | ---------------------------------------- | --------------------------- |
| **Held** | Money held for compliance or risk review | $500 under review |
| **Pending** | Transactions still processing | $1,200 clearing |
| **Available** | Ready to withdraw | $3,400 available for payout |
| **Paid Out** | Sent to your bank account | $10,000 last payout |
Payouts [#payouts]
Account verification is required before your first payout. See [Account Verification](/docs/payouts-verification).
Once verified, request payouts for any amount of $10 or more. Money reaches your bank account in 2-3 business days. Payout fee is 0.25% + $0.25 (Stripe's cost, not ours).
Transaction history [#transaction-history]
Every payment, payout, and status change is recorded as a transaction. Use the finance dashboard to view incoming payments from subscriptions and credit purchases, outgoing payouts, and real-time status updates.
{/* TODO: Add screenshot */}
Related [#related]
* [Account Verification](/docs/payouts-verification) — Get verified to withdraw money
* [Merchant of Record](/docs/merchant-of-record) — How Commet handles taxes and compliance
# Merchant of Record (/docs/merchant-of-record)
A Merchant of Record (MoR) is the legal entity that sells your product to the end customer. Commet acts as the MoR, taking responsibility for global sales taxes, refunds, disputes, and compliance so you can focus on building your product.
PSP vs MoR [#psp-vs-mor]
| Aspect | PSP (e.g. Stripe) | MoR (e.g. Commet) |
| ------------------ | ------------------------------ | ----------------------- |
| Tax handling | You handle it | Platform handles it |
| Refunds & disputes | You handle it | Platform handles it |
| API complexity | Low-level, flexible | High-level, opinionated |
| Fees | Lower per transaction | Higher per transaction |
| Control | Full control over payment flow | Managed payment flow |
What should you choose [#what-should-you-choose]
**Choose a PSP if** you're already integrated with Stripe, comfortable handling international taxes yourself, or want full control over your payment flow.
**Choose Commet if** you want to go live today without worrying about tax registrations, need a billing tool your whole team can use, or want subscription and pricing management built in.
Our vision [#our-vision]
We believe the best companies of the future will be small teams of 5 to 30 people. Technology enables small teams to build incredible products, but monetization complexity shouldn't be a barrier.
Commet exists to be the simplest, developer-preferred tool for monetization. We don't aim to solve every use case — we want to be the tool small teams choose when they want to monetize simply, globally, and without hassle.
Related [#related]
* [Finance Overview](/docs/finance-overview) — Balances, payouts, and transaction history
* [Acceptable Use Policy](/docs/acceptable-use) — What products and services can be sold through Commet
* [Supported Countries](/docs/supported-countries) — 112 countries where Commet operates
# Account Verification (/docs/payouts-verification)
You can start accepting payments immediately, but you need to verify your account before you can withdraw money.
{/* TODO: Add screenshot */}
How it works [#how-it-works]
Click **Verify Account** in the finance section to start the one-time KYC process through Stripe. Provide your business information and bank account details, then wait for approval — usually a few days.
{/* TODO: Add screenshot */}
What you'll need [#what-youll-need]
* **Business information**: Company name, address, registration number
* **Personal information**: For business owners and directors
* **Bank account details**: Where you want payouts sent
Security [#security]
All verification happens through Stripe's secure environment. Stripe is SOC 2 certified, ISO 27001 certified, and PCI DSS compliant.
Approval can take several days. You'll be notified when your account is ready.
Related [#related]
* [Finance Overview](/docs/finance-overview) — Balances, payouts, and transaction history
* [Merchant of Record](/docs/merchant-of-record) — How Commet handles taxes and compliance
# Supported Countries (/docs/supported-countries)
Commet supports businesses in 112 countries across all major regions. Your country is set during onboarding and determines your payout currency and compliance requirements.
Americas [#americas]
Argentina, Bolivia, Brazil, Canada, Chile, Colombia, Costa Rica, Dominican Republic, Ecuador, El Salvador, Guatemala, Guyana, Jamaica, Mexico, Panama, Paraguay, Peru, Trinidad and Tobago, United States, Uruguay
Europe [#europe]
Albania, Austria, Belgium, Bosnia and Herzegovina, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Iceland, Ireland, Italy, Latvia, Liechtenstein, Lithuania, Luxembourg, Malta, Moldova, Montenegro, Netherlands, North Macedonia, Norway, Poland, Portugal, Romania, San Marino, Serbia, Slovakia, Slovenia, Spain, Sweden, Switzerland, United Kingdom
Asia & Pacific [#asia--pacific]
Armenia, Australia, Azerbaijan, Bahrain, Bangladesh, Bhutan, Brunei, Cambodia, Hong Kong, India, Indonesia, Israel, Japan, Jordan, Kazakhstan, South Korea, Kuwait, Laos, Malaysia, Mongolia, New Zealand, Oman, Pakistan, Philippines, Qatar, Singapore, Sri Lanka, Taiwan, Thailand, United Arab Emirates, Vietnam
Africa [#africa]
Algeria, Angola, Benin, Botswana, Côte d'Ivoire, Egypt, Ethiopia, Gabon, Gambia, Ghana, Kenya, Mauritius, Morocco, Mozambique, Namibia, Niger, Nigeria, Rwanda, Senegal, South Africa, Tanzania, Tunisia
Related [#related]
* [Finance Overview](/docs/finance-overview) — How money flows through Commet
* [Account Verification](/docs/payouts-verification) — Get verified to withdraw money
# Integrate with Encore (/docs/integrate-with-encore)
Install [#install]
```bash
encore app create --example=hello-world myapp
cd myapp
go get github.com/commet-labs/commet-go
```
Configure [#configure]
Store secrets using Encore's secret manager instead of environment variables:
```bash
encore secret set --type dev,local,pr,prod CommetAPIKey
encore secret set --type dev,local,pr,prod CommetWebhookSecret
```
```go title="billing/billing.go"
package billing
import (
commet "github.com/commet-labs/commet-go"
)
var secrets struct {
CommetAPIKey string
CommetWebhookSecret string
}
var client *commet.Client
func initClient() error {
var err error
client, err = commet.New(
secrets.CommetAPIKey,
commet.WithEnvironment(commet.Sandbox),
)
return err
}
```
Subscribe [#subscribe]
```go title="billing/billing.go"
import "context"
type SubscribeParams struct {
Email string `json:"email"`
CustomerID string `json:"customer_id"`
}
type SubscribeResponse struct {
CheckoutURL string `json:"checkout_url"`
}
//encore:api public method=POST path=/billing/subscribe
func Subscribe(ctx context.Context, req *SubscribeParams) (*SubscribeResponse, error) {
if err := initClient(); err != nil {
return nil, err
}
_, err := client.Customers.Create(ctx, &commet.CreateCustomerParams{
Email: req.Email,
ID: req.CustomerID,
})
if err != nil {
return nil, err
}
subscription, err := client.Subscriptions.Create(ctx, &commet.CreateSubscriptionParams{
CustomerID: req.CustomerID,
PlanCode: "pro",
})
if err != nil {
return nil, err
}
return &SubscribeResponse{
CheckoutURL: subscription.Data["checkout_url"].(string),
}, nil
}
```
Check Access [#check-access]
```go title="billing/billing.go"
type SubscriptionResponse struct {
Status string `json:"status"`
}
//encore:api public method=GET path=/billing/subscription/:customerID
func GetSubscription(ctx context.Context, customerID string) (*SubscriptionResponse, error) {
if err := initClient(); err != nil {
return nil, err
}
sub, err := client.Subscriptions.Get(ctx, customerID)
if err != nil {
return nil, err
}
return &SubscriptionResponse{
Status: sub.Data["status"].(string),
}, nil
}
type FeatureResponse struct {
Allowed bool `json:"allowed"`
}
//encore:api public method=GET path=/billing/features/:feature/:customerID
func CheckFeature(ctx context.Context, feature string, customerID string) (*FeatureResponse, error) {
if err := initClient(); err != nil {
return nil, err
}
result, err := client.Features.Check(ctx, feature, customerID)
if err != nil {
return nil, err
}
return &FeatureResponse{
Allowed: result.Data["allowed"].(bool),
}, nil
}
```
Track Usage [#track-usage]
```go title="billing/billing.go"
func intPtr(i int) *int { return &i }
type UsageParams struct {
CustomerID string `json:"customer_id"`
}
type UsageResponse struct {
Tracked bool `json:"tracked"`
}
//encore:api public method=POST path=/billing/usage
func TrackUsage(ctx context.Context, req *UsageParams) (*UsageResponse, error) {
if err := initClient(); err != nil {
return nil, err
}
_, err := client.Usage.Track(ctx, &commet.TrackUsageParams{
CustomerID: req.CustomerID,
Feature: "api_calls",
Value: intPtr(1),
})
if err != nil {
return nil, err
}
return &UsageResponse{Tracked: true}, nil
}
```
Usage is aggregated and billed at end of period.
Webhooks [#webhooks]
```go title="billing/webhooks.go"
package billing
import (
"net/http"
commet "github.com/commet-labs/commet-go"
)
//encore:api public raw method=POST path=/webhooks/commet
func HandleWebhook(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
webhooks := &commet.Webhooks{}
payload, err := webhooks.VerifyAndParse(
string(rawBody),
r.Header.Get("x-commet-signature"),
secrets.CommetWebhookSecret,
)
if err != nil {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
switch payload["event"] {
case "subscription.activated":
// handle activation
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`))
}
```
Run [#run]
```bash
encore run
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Go (/docs/integrate-with-go)
Install [#install]
```bash
go get github.com/commet-labs/commet-go
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
COMMET_WEBHOOK_SECRET=whsec_xxx
```
```go title="billing/client.go"
package billing
import (
"log"
"os"
commet "github.com/commet-labs/commet-go"
)
var Client *commet.Client
func Init() {
var err error
Client, err = commet.New(
os.Getenv("COMMET_API_KEY"),
commet.WithEnvironment(commet.Sandbox),
)
if err != nil {
log.Fatal(err)
}
}
```
Subscribe [#subscribe]
```go title="billing/handlers.go"
package billing
import (
"encoding/json"
"net/http"
commet "github.com/commet-labs/commet-go"
)
type subscribeRequest struct {
Email string `json:"email"`
CustomerID string `json:"customer_id"`
}
func Subscribe(w http.ResponseWriter, r *http.Request) {
var req subscribeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, err := Client.Customers.Create(r.Context(), &commet.CreateCustomerParams{
Email: req.Email,
ID: req.CustomerID,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
subscription, err := Client.Subscriptions.Create(r.Context(), &commet.CreateSubscriptionParams{
CustomerID: req.CustomerID,
PlanCode: "pro",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"checkout_url": subscription.Data["checkout_url"]})
}
```
Check Access [#check-access]
```go title="billing/handlers.go"
func GetSubscription(w http.ResponseWriter, r *http.Request) {
customerID := r.PathValue("customerID")
sub, err := Client.Subscriptions.Get(r.Context(), customerID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"status": sub.Data["status"]})
}
func CheckFeature(w http.ResponseWriter, r *http.Request) {
feature := r.PathValue("feature")
customerID := r.PathValue("customerID")
result, err := Client.Features.Check(r.Context(), feature, customerID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"allowed": result.Data["allowed"]})
}
```
Track Usage [#track-usage]
```go title="billing/handlers.go"
func intPtr(i int) *int { return &i }
type usageRequest struct {
CustomerID string `json:"customer_id"`
}
func TrackUsage(w http.ResponseWriter, r *http.Request) {
var req usageRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, err := Client.Usage.Track(r.Context(), &commet.TrackUsageParams{
CustomerID: req.CustomerID,
Feature: "api_calls",
Value: intPtr(1),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"tracked": true})
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```go title="billing/handlers.go"
func Portal(w http.ResponseWriter, r *http.Request) {
result, err := Client.Portal.GetURL(r.Context(), &commet.GetPortalURLParams{
CustomerID: "user_123",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, result.Data["portal_url"].(string), http.StatusTemporaryRedirect)
}
```
Webhooks [#webhooks]
```go title="billing/webhooks.go"
package billing
import (
"io"
"net/http"
"os"
commet "github.com/commet-labs/commet-go"
)
func HandleWebhook(w http.ResponseWriter, r *http.Request) {
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
webhooks := &commet.Webhooks{}
payload, err := webhooks.VerifyAndParse(
string(rawBody),
r.Header.Get("x-commet-signature"),
os.Getenv("COMMET_WEBHOOK_SECRET"),
)
if err != nil {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
switch payload["event"] {
case "subscription.activated":
// handle activation
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`))
}
```
Start Server [#start-server]
```go title="main.go"
package main
import (
"billing"
"log"
"net/http"
)
func main() {
billing.Init()
defer billing.Client.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /billing/subscribe", billing.Subscribe)
mux.HandleFunc("GET /billing/subscription/{customerID}", billing.GetSubscription)
mux.HandleFunc("GET /billing/features/{feature}/{customerID}", billing.CheckFeature)
mux.HandleFunc("POST /billing/usage", billing.TrackUsage)
mux.HandleFunc("GET /billing/portal", billing.Portal)
mux.HandleFunc("POST /webhooks/commet", billing.HandleWebhook)
log.Println("Listening on :3000")
log.Fatal(http.ListenAndServe(":3000", mux))
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Handle Failed Payments (/docs/handle-failed-payments)
When a payment fails, Commet retries automatically while keeping the customer's access active during a grace period.
{/* TODO: Add screenshot */}
How the retry flow works [#how-the-retry-flow-works]
Commet uses Stripe's Smart Retries to attempt the charge multiple times over several days. During this period, the subscription moves to `past_due` status but the customer retains access.
If all retries fail after the grace period, the subscription is canceled automatically.
Check subscription status [#check-subscription-status]
```typescript
const { data } = await commet.subscriptions.get('user_123')
if (data.status === 'past_due') {
// Payment failed, retries in progress
}
```
Gate access based on status [#gate-access-based-on-status]
Most apps should continue granting access during the `past_due` grace period to avoid disrupting customers who simply need to update a card.
```typescript
const { data } = await commet.subscriptions.get('user_123')
const hasAccess = data.status === 'active'
|| data.status === 'trialing'
|| data.status === 'past_due'
```
Prompt payment update [#prompt-payment-update]
Customers in `past_due` can update their payment method through the [Customer Portal](/docs/customer-portal).
```typescript
const portal = await commet.portal.getUrl({ customerId: 'user_123' })
redirect(portal.data.portalUrl)
```
Learn more [#learn-more]
* [What Happens When a Payment Fails](/docs/what-happens-when-a-payment-fails)
Related [#related]
* [Invoices and Billing Cycles](/docs/invoices-and-billing-cycles) — Invoice types and charge timing
* [Manage Subscriptions](/docs/manage-subscriptions) — Create and manage customer subscriptions
* [Customer Portal](/docs/customer-portal) — Self-service billing portal for customers
# Invoices and Billing Cycles (/docs/invoices-and-billing-cycles)
Invoices are the financial records Commet generates automatically whenever a subscription event or billing cycle occurs.
{/* TODO: Add screenshot */}
Invoice types [#invoice-types]
| Type | When Generated | Example |
| ------------------- | -------------------------------------- | ---------------------------------- |
| **Recurring** | Every billing cycle | $99 plan base + $12.50 overage |
| **Overage** | Between cycles (quarterly/yearly only) | 15k extra API calls billed monthly |
| **Plan change** | Customer upgrades mid-cycle | Starter to Pro, prorated $45 |
| **Credit purchase** | Customer buys a credit pack | 500 credits for $40 |
| **Balance top-up** | Customer adds funds | $50 balance top-up |
| **Adjustment** | Manual correction issued | $10 refund for service issue |
When charges happen [#when-charges-happen]
| Component | When Charged | Example |
| -------------------- | ---------------------- | ----------------------------- |
| **Plan base price** | Advance (period start) | $99 on Jan 1 |
| **Boolean features** | Included in plan base | SSO, Custom Branding |
| **Metered overage** | True-up (period end) | 2,500 extra API calls |
| **Included seats** | Advance (with base) | 5 editor seats |
| **Additional seats** | True-up (period end) | 3 extra seats added mid-cycle |
Billing intervals [#billing-intervals]
| Interval | Base Invoice | Overage Invoice | Example |
| ------------- | --------------- | ----------------------------- | ------------------------------------ |
| **Monthly** | Every month | Included in recurring invoice | $99/month on the 1st |
| **Quarterly** | Every 3 months | Monthly | $297 base quarterly, overage monthly |
| **Yearly** | Every 12 months | Monthly | $899 base yearly, overage monthly |
For quarterly and yearly plans, overage is calculated and invoiced monthly even though the base charge is less frequent.
Learn more [#learn-more]
* [What Invoices Do Customers Receive and When](/docs/what-invoices-do-customers-receive-and-when)
* [How Do Monthly, Quarterly, and Yearly Billing Work](/docs/how-do-monthly-quarterly-and-yearly-billing-work)
Related [#related]
* [Handle Failed Payments](/docs/handle-failed-payments) — Retry flow and grace periods for failed charges
* [Consumption Models](/docs/consumption-models) — Metered, Credits, and Balance explained
* [Finance Overview](/docs/finance-overview) — How money flows through Commet
# Integrate with Java (/docs/integrate-with-java)
Install [#install]
```xml title="pom.xml"
co.commetcommet-java0.1.0
```
```kotlin title="build.gradle.kts"
implementation("co.commet:commet-java:0.1.0")
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```java title="CommetClient.java"
import co.commet.Commet;
import co.commet.Environment;
public class CommetClient {
public static final Commet commet = Commet.builder()
.apiKey(System.getenv("COMMET_API_KEY"))
.environment(Environment.SANDBOX)
.build();
}
```
Create Customer and Subscribe [#create-customer-and-subscribe]
`customers().create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```java
import co.commet.ApiResponse;
import java.util.Map;
commet.customers().create("user@example.com", "user_123");
ApiResponse subscription = commet.subscriptions().create(
"user_123", "pro",
null, null, null, null, null, null, null
);
Map data = (Map) subscription.getData();
String checkoutUrl = (String) data.get("checkout_url");
```
The customer is redirected to checkout to complete payment.
Check Access [#check-access]
```java
ApiResponse sub = commet.subscriptions().get("user_123");
Map subData = (Map) sub.getData();
String status = (String) subData.get("status");
ApiResponse access = commet.features().check("custom_branding", "user_123");
Map accessData = (Map) access.getData();
boolean allowed = (boolean) accessData.get("allowed");
```
Track Usage [#track-usage]
```java
commet.usage().track(
"api_calls", "user_123",
1, null, null, null, null, null, null, null, null
);
```
Usage is aggregated and billed at end of period.
Webhooks [#webhooks]
```java
import co.commet.resources.Webhooks;
import java.util.Map;
Webhooks webhooks = new Webhooks();
Map payload = webhooks.verifyAndParse(
rawBody, signature, webhookSecret
);
if (payload == null) {
// return 401
}
if ("subscription.activated".equals(payload.get("event"))) {
// handle activation
}
```
Related [#related]
* [SDK Reference](/docs/sdk-reference)
# Integrate with Astro (/docs/integrate-with-astro)
Astro requires an SSR adapter for API routes. Set `output: 'server'` or `output: 'hybrid'` in your `astro.config.mjs`.
Install [#install]
```bash
pnpm add @commet/node
```
```bash
npm install @commet/node
```
```bash
yarn add @commet/node
```
```bash
bun add @commet/node
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="src/lib/commet.ts"
import { Commet } from '@commet/node'
export const commet = new Commet({
apiKey: import.meta.env.COMMET_API_KEY,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
`customers.create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```typescript title="src/pages/api/billing/subscribe.ts"
import type { APIRoute } from 'astro'
import { commet } from '../../../lib/commet'
export const POST: APIRoute = async ({ request }) => {
const { customerId, email } = await request.json()
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
return Response.json({ checkoutUrl: subscription.data.checkoutUrl })
}
```
Check Access [#check-access]
```typescript title="src/pages/api/billing/access/[customerId].ts"
import type { APIRoute } from 'astro'
import { commet } from '../../../../lib/commet'
export const GET: APIRoute = async ({ params }) => {
const { data: subscription } = await commet.subscriptions.get(params.customerId!)
const { data: feature } = await commet.features.check({
code: 'api_calls',
customerId: params.customerId!,
})
return Response.json({
status: subscription.status,
allowed: feature.allowed,
})
}
```
Track Usage [#track-usage]
```typescript title="src/pages/api/billing/usage.ts"
import type { APIRoute } from 'astro'
import { commet } from '../../../lib/commet'
export const POST: APIRoute = async ({ request }) => {
const { customerId } = await request.json()
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
return Response.json({ tracked: true })
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="src/pages/api/billing/portal.ts"
import type { APIRoute } from 'astro'
import { commet } from '../../../lib/commet'
export const GET: APIRoute = async ({ redirect }) => {
const customerId = 'user_123'
const { data } = await commet.portal.getUrl({ customerId })
return redirect(data.portalUrl)
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Bun (/docs/integrate-with-bun)
Install [#install]
```bash
bun add @commet/node
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="src/commet.ts"
import { Commet } from '@commet/node'
export const commet = new Commet({
apiKey: Bun.env.COMMET_API_KEY!,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
```typescript title="src/index.ts"
import { commet } from './commet'
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url)
if (url.pathname === '/subscribe' && req.method === 'POST') {
const { customerId, email } = await req.json()
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
return Response.json({ checkoutUrl: subscription.data.checkoutUrl })
}
return new Response('Not Found', { status: 404 })
},
})
```
Check Access [#check-access]
Add these routes to the `fetch` handler.
```typescript title="src/index.ts"
if (url.pathname.startsWith('/subscription/') && req.method === 'GET') {
const customerId = url.pathname.split('/')[2]
const { data } = await commet.subscriptions.get(customerId)
return Response.json({ status: data.status })
}
if (url.pathname.startsWith('/features/') && req.method === 'GET') {
const [, , feature, customerId] = url.pathname.split('/')
const { data } = await commet.features.check({ code: feature, customerId })
return Response.json({ allowed: data.allowed })
}
```
Track Usage [#track-usage]
```typescript title="src/index.ts"
if (url.pathname === '/usage' && req.method === 'POST') {
const { customerId } = await req.json()
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
return Response.json({ tracked: true })
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="src/index.ts"
if (url.pathname === '/portal' && req.method === 'GET') {
const customerId = url.searchParams.get('customerId')!
const { data } = await commet.portal.getUrl({ customerId })
return Response.redirect(data.portalUrl)
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Express (/docs/integrate-with-express)
Install [#install]
```bash
pnpm add @commet/node express
```
```bash
npm install @commet/node express
```
```bash
yarn add @commet/node express
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="src/commet.ts"
import { Commet } from '@commet/node'
export const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
```typescript title="src/routes/billing.ts"
import { Router } from 'express'
import { commet } from '../commet'
const router = Router()
router.post('/subscribe', async (req, res) => {
const { customerId, email } = req.body
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
res.json({ checkoutUrl: subscription.data.checkoutUrl })
})
export default router
```
Check Access [#check-access]
```typescript title="src/routes/billing.ts"
router.get('/subscription/:customerId', async (req, res) => {
const { data } = await commet.subscriptions.get(req.params.customerId)
res.json({ status: data.status })
})
router.get('/features/:feature/:customerId', async (req, res) => {
const { data } = await commet.features.check({
code: req.params.feature,
customerId: req.params.customerId,
})
res.json({ allowed: data.allowed })
})
```
Track Usage [#track-usage]
```typescript title="src/routes/billing.ts"
router.post('/usage', async (req, res) => {
await commet.usage.track({
customerId: req.body.customerId,
feature: 'api_calls',
value: 1,
})
res.json({ tracked: true })
})
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="src/routes/billing.ts"
router.get('/portal', async (req, res) => {
const { data } = await commet.portal.getUrl({
customerId: req.user.customerId,
})
res.redirect(data.portalUrl)
})
```
Start Server [#start-server]
```typescript title="src/index.ts"
import express from 'express'
import billingRoutes from './routes/billing'
const app = express()
app.use(express.json())
app.use('/billing', billingRoutes)
app.listen(3000)
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Hono (/docs/integrate-with-hono)
Install [#install]
```bash
pnpm add @commet/node hono
```
```bash
npm install @commet/node hono
```
```bash
bun add @commet/node hono
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="src/commet.ts"
import { Commet } from '@commet/node'
export const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
```typescript title="src/routes/billing.ts"
import { Hono } from 'hono'
import { commet } from '../commet'
const billing = new Hono()
billing.post('/subscribe', async (c) => {
const { customerId, email } = await c.req.json()
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
return c.json({ checkoutUrl: subscription.data.checkoutUrl })
})
export default billing
```
Check Access [#check-access]
```typescript title="src/routes/billing.ts"
billing.get('/subscription/:customerId', async (c) => {
const { data } = await commet.subscriptions.get(c.req.param('customerId'))
return c.json({ status: data.status })
})
billing.get('/features/:feature/:customerId', async (c) => {
const { data } = await commet.features.check({
code: c.req.param('feature'),
customerId: c.req.param('customerId'),
})
return c.json({ allowed: data.allowed })
})
```
Track Usage [#track-usage]
```typescript title="src/routes/billing.ts"
billing.post('/usage', async (c) => {
const { customerId } = await c.req.json()
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
return c.json({ tracked: true })
})
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="src/routes/billing.ts"
billing.get('/portal', async (c) => {
const customerId = c.get('customerId')
const { data } = await commet.portal.getUrl({ customerId })
return c.redirect(data.portalUrl)
})
```
Start Server [#start-server]
```typescript title="src/index.ts"
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import billing from './routes/billing'
const app = new Hono()
app.route('/billing', billing)
serve({ fetch: app.fetch, port: 3000 })
```
```typescript title="src/index.ts"
import { Hono } from 'hono'
import billing from './routes/billing'
const app = new Hono()
app.route('/billing', billing)
export default app
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Next.js (/docs/integrate-with-nextjs)
Install [#install]
```bash
pnpm add @commet/node @commet/next
```
```bash
npm install @commet/node @commet/next
```
```bash
yarn add @commet/node @commet/next
```
```bash
bun add @commet/node @commet/next
```
Configure [#configure]
```bash title=".env.local"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="lib/commet.ts"
import { Commet } from '@commet/node'
export const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
environment: 'sandbox',
})
```
Create Customer and Subscribe [#create-customer-and-subscribe]
`customers.create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```typescript title="app/actions/billing.ts"
'use server'
import { redirect } from 'next/navigation'
import { commet } from '@/lib/commet'
export async function subscribe(customerId: string) {
await commet.customers.create({
email: 'user@example.com',
id: customerId,
})
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
redirect(subscription.data.checkoutUrl!)
}
```
The customer is redirected to checkout to complete payment.
Check Access [#check-access]
```typescript title="app/actions/billing.ts"
export async function getSubscription(customerId: string) {
const { data } = await commet.subscriptions.get(customerId)
return data
}
```
```typescript title="app/actions/features.ts"
export async function canUseFeature(customerId: string, feature: string) {
const { data } = await commet.features.check({ code: feature, customerId })
return data.allowed
}
```
Track Usage [#track-usage]
```typescript title="app/actions/usage.ts"
export async function trackApiCall(customerId: string) {
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="app/api/commet/portal/route.ts"
import { CustomerPortal } from '@commet/next'
export const GET = CustomerPortal({
apiKey: process.env.COMMET_API_KEY!,
getCustomerId: async (req) => {
return 'user_123'
},
})
```
```tsx
Manage Billing
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Nuxt (/docs/integrate-with-nuxt)
Install [#install]
```bash
pnpm add @commet/node
```
```bash
npm install @commet/node
```
```bash
yarn add @commet/node
```
```bash
bun add @commet/node
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="nuxt.config.ts"
export default defineNuxtConfig({
runtimeConfig: {
commetApiKey: process.env.COMMET_API_KEY,
},
})
```
Nuxt auto-imports from `server/utils/`, so the client is available in all server routes.
```typescript title="server/utils/commet.ts"
import { Commet } from '@commet/node'
const config = useRuntimeConfig()
export const commet = new Commet({
apiKey: config.commetApiKey,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
`customers.create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```typescript title="server/api/billing/subscribe.post.ts"
export default defineEventHandler(async (event) => {
const { customerId, email } = await readBody(event)
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
return { checkoutUrl: subscription.data.checkoutUrl }
})
```
Check Access [#check-access]
```typescript title="server/api/billing/access/[customerId].get.ts"
export default defineEventHandler(async (event) => {
const customerId = getRouterParam(event, 'customerId')!
const { data: subscription } = await commet.subscriptions.get(customerId)
const { data: feature } = await commet.features.check({
code: 'api_calls',
customerId,
})
return {
status: subscription.status,
allowed: feature.allowed,
}
})
```
Track Usage [#track-usage]
```typescript title="server/api/billing/usage.post.ts"
export default defineEventHandler(async (event) => {
const { customerId } = await readBody(event)
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
return { tracked: true }
})
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="server/api/billing/portal.get.ts"
export default defineEventHandler(async (event) => {
const customerId = 'user_123'
const { data } = await commet.portal.getUrl({ customerId })
return sendRedirect(event, data.portalUrl)
})
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Remix (/docs/integrate-with-remix)
Install [#install]
```bash
pnpm add @commet/node @remix-run/node
```
```bash
npm install @commet/node @remix-run/node
```
```bash
yarn add @commet/node @remix-run/node
```
```bash
bun add @commet/node @remix-run/node
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
The `.server.ts` suffix ensures this module is never bundled into client code.
```typescript title="app/lib/commet.server.ts"
import { Commet } from '@commet/node'
export const commet = new Commet({
apiKey: process.env.COMMET_API_KEY!,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
`customers.create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```typescript title="app/routes/billing.subscribe.ts"
import { redirect, type ActionFunctionArgs } from '@remix-run/node'
import { commet } from '~/lib/commet.server'
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const customerId = String(formData.get('customerId'))
const email = String(formData.get('email'))
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
return redirect(subscription.data.checkoutUrl!)
}
```
Check Access [#check-access]
```typescript title="app/routes/billing.status.ts"
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { commet } from '~/lib/commet.server'
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url)
const customerId = url.searchParams.get('customerId')!
const { data: subscription } = await commet.subscriptions.get(customerId)
const { data: feature } = await commet.features.check({
code: 'api_calls',
customerId,
})
return json({
status: subscription.status,
allowed: feature.allowed,
})
}
```
Track Usage [#track-usage]
```typescript title="app/routes/billing.usage.ts"
import { json, type ActionFunctionArgs } from '@remix-run/node'
import { commet } from '~/lib/commet.server'
export async function action({ request }: ActionFunctionArgs) {
const { customerId } = await request.json()
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
return json({ tracked: true })
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="app/routes/billing.portal.ts"
import { redirect, type LoaderFunctionArgs } from '@remix-run/node'
import { commet } from '~/lib/commet.server'
export async function loader({ request }: LoaderFunctionArgs) {
const customerId = 'user_123'
const { data } = await commet.portal.getUrl({ customerId })
return redirect(data.portalUrl)
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with SvelteKit (/docs/integrate-with-sveltekit)
Install [#install]
```bash
pnpm add @commet/node
```
```bash
npm install @commet/node
```
```bash
yarn add @commet/node
```
```bash
bun add @commet/node
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```typescript title="src/lib/server/commet.ts"
import { Commet } from '@commet/node'
import { COMMET_API_KEY } from '$env/static/private'
export const commet = new Commet({
apiKey: COMMET_API_KEY,
environment: 'sandbox',
})
```
Subscribe [#subscribe]
`customers.create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```typescript title="src/routes/api/billing/subscribe/+server.ts"
import { json } from '@sveltejs/kit'
import { commet } from '$lib/server/commet'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ request }) => {
const { customerId, email } = await request.json()
await commet.customers.create({ email, id: customerId })
const subscription = await commet.subscriptions.create({
customerId,
planCode: 'pro',
})
return json({ checkoutUrl: subscription.data.checkoutUrl })
}
```
Check Access [#check-access]
```typescript title="src/routes/api/billing/access/[customerId]/+server.ts"
import { json } from '@sveltejs/kit'
import { commet } from '$lib/server/commet'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ params }) => {
const { data: subscription } = await commet.subscriptions.get(params.customerId)
const { data: feature } = await commet.features.check({
code: 'api_calls',
customerId: params.customerId,
})
return json({ status: subscription.status, allowed: feature.allowed })
}
```
Track Usage [#track-usage]
```typescript title="src/routes/api/billing/usage/+server.ts"
import { json } from '@sveltejs/kit'
import { commet } from '$lib/server/commet'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ request }) => {
const { customerId } = await request.json()
await commet.usage.track({
customerId,
feature: 'api_calls',
value: 1,
})
return json({ tracked: true })
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```typescript title="src/routes/api/billing/portal/+server.ts"
import { redirect } from '@sveltejs/kit'
import { commet } from '$lib/server/commet'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ locals }) => {
const { data } = await commet.portal.getUrl({
customerId: locals.user.customerId,
})
redirect(303, data.portalUrl)
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Laravel (/docs/integrate-with-laravel)
Install [#install]
```bash
composer require commet/commet-php
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
COMMET_WEBHOOK_SECRET=whsec_xxx
```
```php title="app/Providers/AppServiceProvider.php"
app->singleton(Commet::class, function () {
return new Commet(
apiKey: config('services.commet.api_key'),
environment: app()->environment('production') ? 'live' : 'sandbox',
);
});
}
}
```
```php title="config/services.php"
'commet' => [
'api_key' => env('COMMET_API_KEY'),
'webhook_secret' => env('COMMET_WEBHOOK_SECRET'),
],
```
Subscribe [#subscribe]
```php title="app/Http/Controllers/BillingController.php"
validate([
'email' => 'required|email',
'customer_id' => 'required|string',
]);
$this->commet->customers->create(
email: $request->input('email'),
id: $request->input('customer_id'),
);
$subscription = $this->commet->subscriptions->create(
customerId: $request->input('customer_id'),
planCode: 'pro',
);
return response()->json([
'checkout_url' => $subscription->data['checkout_url'],
]);
}
}
```
Check Access [#check-access]
```php title="app/Http/Controllers/BillingController.php"
public function getSubscription(string $customerId): JsonResponse
{
$subscription = $this->commet->subscriptions->get($customerId);
return response()->json([
'status' => $subscription->data['status'],
]);
}
public function checkFeature(string $feature, string $customerId): JsonResponse
{
$result = $this->commet->features->check(code: $feature, customerId: $customerId);
return response()->json([
'allowed' => $result->data['allowed'],
]);
}
```
Track Usage [#track-usage]
```php title="app/Http/Controllers/BillingController.php"
public function trackUsage(Request $request): JsonResponse
{
$request->validate([
'customer_id' => 'required|string',
]);
$this->commet->usage->track(
customerId: $request->input('customer_id'),
feature: 'api_calls',
value: 1,
);
return response()->json(['tracked' => true]);
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```php title="app/Http/Controllers/BillingController.php"
use Illuminate\Http\RedirectResponse;
public function portal(): RedirectResponse
{
$result = $this->commet->portal->getUrl(customerId: 'user_123');
return redirect($result->data['portal_url']);
}
```
Webhooks [#webhooks]
```php title="app/Http/Controllers/WebhookController.php"
verifyAndParse(
rawBody: $request->getContent(),
signature: $request->header('x-commet-signature'),
secret: config('services.commet.webhook_secret'),
);
if ($payload === null) {
return response()->json(['error' => 'Invalid signature'], 401);
}
match ($payload['event']) {
'subscription.activated' => $this->handleActivated($payload),
'subscription.canceled' => $this->handleCanceled($payload),
default => null,
};
return response()->json(['ok' => true]);
}
private function handleActivated(array $payload): void
{
// handle activation
}
private function handleCanceled(array $payload): void
{
// handle cancellation
}
}
```
Exclude the webhook route from CSRF verification:
```php title="bootstrap/app.php"
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'webhooks/commet',
]);
})
```
Routes [#routes]
```php title="routes/api.php"
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with PHP (/docs/integrate-with-php)
Install [#install]
```bash
composer require commet/commet-php
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```php title="commet.php"
Create Customer and Subscribe [#create-customer-and-subscribe]
`customers->create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```php
$commet->customers->create(
email: 'user@example.com',
id: 'user_123',
);
$subscription = $commet->subscriptions->create(
customerId: 'user_123',
planCode: 'pro',
);
$checkoutUrl = $subscription->data['checkout_url'];
```
The customer is redirected to checkout to complete payment.
Check Access [#check-access]
```php
$sub = $commet->subscriptions->get('user_123');
$status = $sub->data['status'];
$access = $commet->features->check(code: 'custom_branding', customerId: 'user_123');
$allowed = $access->data['allowed'];
```
Track Usage [#track-usage]
```php
$commet->usage->track(
customerId: 'user_123',
feature: 'api_calls',
value: 1,
);
```
Usage is aggregated and billed at end of period.
Webhooks [#webhooks]
```php
verifyAndParse(
rawBody: file_get_contents('php://input'),
signature: $_SERVER['HTTP_X_COMMET_SIGNATURE'] ?? '',
secret: $_ENV['COMMET_WEBHOOK_SECRET'],
);
if ($payload === null) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
match ($payload['event']) {
'subscription.activated' => handleActivated($payload),
'subscription.canceled' => handleCanceled($payload),
default => null,
};
echo json_encode(['ok' => true]);
```
Related [#related]
* [Laravel](/docs/integrate-with-laravel)
* [Symfony](/docs/integrate-with-symfony)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Symfony (/docs/integrate-with-symfony)
Install [#install]
```bash
composer require commet/commet-php
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
COMMET_WEBHOOK_SECRET=whsec_xxx
```
```yaml title="config/services.yaml"
services:
Commet\Commet:
factory: ['@App\Factory\CommetFactory', 'create']
App\Factory\CommetFactory:
arguments:
$apiKey: '%env(COMMET_API_KEY)%'
$environment: '%kernel.environment%'
```
```php title="src/Factory/CommetFactory.php"
apiKey,
environment: $this->environment === 'prod' ? 'live' : 'sandbox',
);
}
}
```
Subscribe [#subscribe]
```php title="src/Controller/BillingController.php"
toArray();
$this->commet->customers->create(
email: $data['email'],
id: $data['customer_id'],
);
$subscription = $this->commet->subscriptions->create(
customerId: $data['customer_id'],
planCode: 'pro',
);
return $this->json([
'checkout_url' => $subscription->data['checkout_url'],
]);
}
}
```
Check Access [#check-access]
```php title="src/Controller/BillingController.php"
#[Route('/billing/subscription/{customerId}', methods: ['GET'])]
public function getSubscription(string $customerId): JsonResponse
{
$subscription = $this->commet->subscriptions->get($customerId);
return $this->json([
'status' => $subscription->data['status'],
]);
}
#[Route('/billing/features/{feature}/{customerId}', methods: ['GET'])]
public function checkFeature(string $feature, string $customerId): JsonResponse
{
$result = $this->commet->features->check(code: $feature, customerId: $customerId);
return $this->json([
'allowed' => $result->data['allowed'],
]);
}
```
Track Usage [#track-usage]
```php title="src/Controller/BillingController.php"
#[Route('/billing/usage', methods: ['POST'])]
public function trackUsage(Request $request): JsonResponse
{
$data = $request->toArray();
$this->commet->usage->track(
customerId: $data['customer_id'],
feature: 'api_calls',
value: 1,
);
return $this->json(['tracked' => true]);
}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```php title="src/Controller/BillingController.php"
use Symfony\Component\HttpFoundation\RedirectResponse;
#[Route('/billing/portal', methods: ['GET'])]
public function portal(): RedirectResponse
{
$result = $this->commet->portal->getUrl(customerId: 'user_123');
return $this->redirect($result->data['portal_url']);
}
```
Webhooks [#webhooks]
```php title="src/Controller/WebhookController.php"
verifyAndParse(
rawBody: $request->getContent(),
signature: $request->headers->get('x-commet-signature'),
secret: $this->webhookSecret,
);
if ($payload === null) {
return new Response('Invalid signature', 401);
}
match ($payload['event']) {
'subscription.activated' => $this->handleActivated($payload),
'subscription.canceled' => $this->handleCanceled($payload),
default => null,
};
return $this->json(['ok' => true]);
}
private function handleActivated(array $payload): void
{
// handle activation
}
private function handleCanceled(array $payload): void
{
// handle cancellation
}
}
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Add-ons (/docs/add-ons)
Add-ons are optional features with their own price and consumption model that customers activate on their subscriptions. They extend a subscription without modifying the base plan — think SSO, SMS channels, or premium support.
{/* TODO: Add screenshot */}
How add-ons work [#how-add-ons-work]
| Aspect | Description |
| ------------------------ | ----------------------------------------------------------- |
| **Pricing** | Fixed base price per billing period, prorated on activation |
| **Charge on activation** | Immediate charge for remaining days in current period |
| **Recurring billing** | Base price added to the plan's invoice each cycle |
| **Deactivation** | No refund — the feature stops immediately |
| **Feature access** | The add-on's feature appears alongside plan features |
Each add-on maps to exactly one feature. When activated, that feature becomes available through `features.get`, `features.check`, and `features.list` — no different from a plan feature.
Consumption models [#consumption-models]
Add-ons declare their own consumption model. Boolean add-ons are compatible with any plan. All other models require matching the plan's model.
| Model | Description | Compatible Plans | Example |
| ----------- | ------------------------------------------ | ---------------- | ------------------------------- |
| **Boolean** | Unlocks access, no usage tracking | All plans | SSO, HIPAA compliance |
| **Metered** | Included units + overage at period end | Metered plans | SMS: 1000 included, $0.03/extra |
| **Credits** | Usage consumes from the plan's credit pool | Credits plans | AI summaries: 5 credits/use |
| **Balance** | Usage deducts from the plan's balance pool | Balance plans | Image processing: $0.015/unit |
Credits and balance add-ons consume from the plan's **shared pool** — there's no separate pool for the add-on. If the pool runs out, the add-on's feature is blocked too.
Create add-ons in the dashboard [#create-add-ons-in-the-dashboard]
Go to **Add-ons** and click **Create Add-on**. Configure the name, base price, feature, and consumption model. For metered add-ons, set included units and overage rate. For credits, set the credit cost per unit.
The feature dropdown only shows features not already assigned to another add-on. Once created, the add-on is available to any customer whose plan is compatible.
{/* TODO: Add screenshot */}
Availability by subscription status [#availability-by-subscription-status]
Add-ons can be activated on any subscription with a payment method:
| Status | Can activate add-ons |
| ------------- | ------------------------------------------------- |
| **Active** | Yes |
| **Trialing** | Yes — card was captured during trial checkout |
| **Free plan** | Yes — first purchase prompts for a payment method |
Manage add-ons [#manage-add-ons]
Add-ons are managed through the **dashboard** and the **customer portal**. Activation and deactivation are not available via the API or SDK.
| Action | Where |
| ---------------------------- | -------------------------------------------------- |
| **Create / archive add-ons** | Dashboard → Add-ons |
| **Activate / deactivate** | Dashboard (subscription detail) or Customer Portal |
| **List active add-ons** | API, SDK, Dashboard, or Customer Portal |
List active add-ons via SDK [#list-active-add-ons-via-sdk]
```typescript
const active = await customer.addons.list()
```
**Response:**
```json
{
"success": true,
"data": [
{
"slug": "sso-access",
"name": "SSO Access",
"basePrice": 5000,
"featureCode": "sso",
"featureName": "Single Sign-On",
"featureType": "boolean",
"consumptionModel": "boolean",
"activatedAt": "2026-03-11T00:00:00.000Z"
}
]
}
```
Pass either a Commet ID (`cus_xxx`) or your external ID as `customerId` to identify the customer.
Feature access [#feature-access]
Add-on features work exactly like plan features — no special handling needed:
```typescript
const customer = commet.customer('user_123')
// Check boolean add-on
const sso = await customer.features.get('sso')
// { access: true, type: 'boolean', enabled: true }
// Track metered add-on usage
await customer.usage.track('sms_messages', 50)
// List all features (plan + add-ons combined)
const features = await customer.features.list()
```
Billing behavior [#billing-behavior]
Activation charge [#activation-charge]
When a customer activates a $50/month add-on on day 11 of a 31-day period (20 days remaining):
| | Value |
| ---------------- | -------------------------- |
| **Full price** | $50.00 |
| **Prorated** | $50 × (20/31) = **$32.26** |
| **Invoice type** | `addon_activation` |
The charge goes through Stripe immediately with its own invoice.
Recurring invoices [#recurring-invoices]
Starting from the next full billing cycle, the add-on base price appears as a separate line in the plan's invoice:
```
Plan Pro (base) $99.00
API Calls: 12,500 (2,500 overage × $0.01) $25.00
Add-ons
SMS Channel (base) $15.00
SMS: 1,800 (800 overage × $0.03) $24.00
SSO $50.00
Subtotal $213.00
```
Multi-currency [#multi-currency]
Add-on prices are defined in USD. For non-USD subscriptions, the price is converted using the plan's exchange rate — the same mechanism used for plan base prices.
Customer portal [#customer-portal]
Customers can self-service add-ons from the portal:
* **Available add-ons** — see compatible add-ons with pricing
* **Activate** — confirmation dialog with prorated charge preview
* **Active add-ons** — manage active add-ons
* **Deactivate** — instant, no refund
Add-ons whose feature already exists in the customer's plan are hidden from the portal automatically.
Learn more [#learn-more]
* [How Does Billing Work](/docs/how-does-billing-work)
Related [#related]
* [Consumption Models](/docs/consumption-models) — Metered, Credits, and Balance explained
* [Configure Features](/docs/configure-features) — Define features that add-ons can unlock
* [Credit Packs](/docs/credit-packs) — Another way to extend plan capabilities
* [Manage Subscriptions](/docs/manage-subscriptions) — Subscription lifecycle and management
* [Customer Portal](/docs/customer-portal) — Where customers activate add-ons
# Configure Features (/docs/configure-features)
Features are reusable building blocks that define the capabilities you sell. Create a feature once and add it to multiple plans with different configurations.
Feature types [#feature-types]
| Type | Description | Examples |
| ----------- | --------------------------------- | --------------------------------------- |
| **Boolean** | On/off access to a capability | SSO, Custom Branding, Priority Support |
| **Metered** | Usage-based with included amounts | API Calls, Storage GB, Emails Sent |
| **Seats** | Per-user licenses | Editor Seats, Admin Seats, Viewer Seats |
Consumption is defined at the plan level, not the feature level. When you add a feature to a plan, you specify how much is included, whether there's overage, and at what price.
{/* TODO: Add screenshot */}
Create features in the dashboard [#create-features-in-the-dashboard]
Go to **Features** and click **Create Feature**. Every feature needs a **Name** (customer-facing) and a **Code** (internal identifier for the SDK, e.g., `api_calls`). Feature codes only accept lowercase letters, numbers, and underscores (`^[a-z0-9_]+$`) and must be unique within your organization.
Metered features also require an **Event Code** and **Unit Name** (e.g., "calls", "GB"). Seat features optionally accept a **Seat Type** (e.g., "Editor", "Admin").
{/* TODO: Add screenshot */}
Check feature access [#check-feature-access]
Gate features based on the customer's plan.
```typescript
const { data } = await commet.features.check({
code: 'custom_branding',
customerId: 'user_123',
})
if (!data.allowed) {
redirect('/upgrade')
}
```
Get feature details [#get-feature-details]
Returns usage numbers, limits, and overage info.
```typescript
const { data } = await commet.features.get({
code: 'api_calls',
customerId: 'user_123',
})
// data.current — usage this period
// data.included — included in plan
// data.remaining — units left
// data.overage — units over the limit
// data.unlimited — true if no cap
```
Pre-flight check [#pre-flight-check]
Check if a customer can use a feature **and** whether they'll be charged extra.
```typescript
const { data } = await commet.features.canUse({
code: 'team_members',
customerId: 'user_123',
})
if (!data.allowed) {
return { error: 'Upgrade to add more members' }
}
if (data.willBeCharged) {
// Show overage confirmation
}
```
List all features [#list-all-features]
```typescript
const { data } = await commet.features.list('user_123')
```
Related [#related]
* [Track Usage](/docs/track-usage) — Send usage events for metered features
* [Manage Plans](/docs/create-plans) — Add features to pricing plans
* [Consumption Models](/docs/consumption-models) — How features behave in each model
* [Seat Management](/docs/seat-management) — Manage seat-based licenses
# Consumption Models (/docs/consumption-models)
Every plan uses one consumption model that defines how customers consume features and how they're billed. Models are mutually exclusive.
The three models [#the-three-models]
| Model | Description | Example Products |
| ----------- | ----------------------------------------------------------------------------- | --------------------------- |
| **Metered** | Base price + included usage. Overage charged at period end | AWS, Twilio, SendGrid |
| **Credits** | Base price includes credits. Usage consumes credits. Buy packs when exhausted | ChatGPT, Midjourney, Jasper |
| **Balance** | Base price becomes a spending balance. Usage deducts real dollars | Google Cloud, Anthropic |
Metered [#metered]
Customers pay a base price and get included usage. Overage beyond the included amount is charged at the end of the billing period.
| Feature | Included | Overage Price |
| ----------- | ------------ | ---------------- |
| API Calls | 10,000/month | $0.01 per call |
| Storage | 100 GB | $0.10 per GB |
| Email Sends | 50,000/month | $0.001 per email |
Credits [#credits]
Customers receive credits with their subscription. Feature usage consumes credits. When credits run out, customers can purchase [Credit Packs](/docs/credit-packs) or wait for the next billing cycle.
| Feature | Credits per Use |
| ------------------- | --------------- |
| AI Image Generation | 10 credits |
| AI Text Generation | 2 credits |
| AI Voice Synthesis | 25 credits |
Plan credits reset each billing period. Credits purchased via Credit Packs **never expire**.
Balance [#balance]
Customers pay a base price that becomes their spending balance. Feature usage costs real money deducted from the balance. Overage is charged at period end.
Balance supports two pricing modes per feature:
| Pricing Mode | How price is determined | Best for |
| --------------- | ------------------------------------------------------- | ------------------------------ |
| **Fixed Price** | You set a price per unit | API calls, storage, processing |
| **AI Model** | Commet calculates from model token prices + your margin | AI-powered features |
Fixed pricing [#fixed-pricing]
| Feature | Cost per Use |
| --------------------------- | ------------ |
| API Call | $0.001 |
| Image Processing | $0.05 |
| Video Encoding (per minute) | $0.10 |
AI Model pricing [#ai-model-pricing]
Set a margin percentage instead of a fixed price. Commet looks up the model's token cost and applies your margin automatically. See [AI Token Billing](/docs/ai-token-billing) for details.
Comparison [#comparison]
| Aspect | Metered | Credits | Balance |
| -------------------- | --------------------- | ----------------------- | ----------------------------- |
| **When exceeded** | Overage at period end | Blocked or buy packs | Overage at period end |
| **Reset behavior** | Usage resets to 0 | Plan credits reset | Balance resets to plan amount |
| **Purchased extras** | N/A | Credits persist forever | Top-ups reset at period |
| **Customer portal** | View usage | Buy credit packs | Add balance (top-up) |
Overage restrictions by plan type [#overage-restrictions-by-plan-type]
| Plan type | Overage |
| -------------------------- | ------------------------------------------------------------------------- |
| **Paid plan** | Fully supported |
| **Free plan** | **Not allowed** — usage is blocked at included limits |
| **Trial** (on a paid plan) | **Blocked during trial** — activates when the subscription becomes active |
See [Free Plans](/docs/how-do-free-plans-work-without-payment) and [Trials](/docs/how-do-trial-periods-work) for details.
Learn more [#learn-more]
* [How Does Billing Work](/docs/how-does-billing-work)
Related [#related]
* [Manage Plans](/docs/create-plans) — Create plans with consumption models
* [Credit Packs](/docs/credit-packs) — Configure credit packages for credits-based plans
* [Configure Features](/docs/configure-features) — Define what customers can access
* [Customer Portal](/docs/customer-portal) — Where customers manage their consumption
# Manage Plans (/docs/create-plans)
Plans are pre-configured billing packages that combine pricing, features, and billing intervals. Assign a plan to a customer and Commet creates the subscription, customer portal, and recurring invoices automatically.
{/* TODO: Add screenshot */}
Plan components [#plan-components]
| Component | Description | Example |
| ---------------------- | ------------------------------------------------- | ------------------------------ |
| **Name & Description** | Customer-facing display information | "Pro Plan — For growing teams" |
| **Consumption Model** | How customers consume and pay for features | Metered, Credits, or Balance |
| **Prices** | Pricing options by billing interval | $99/month, $899/year |
| **Features** | What's included — boolean, metered, or seat-based | API Calls (10k), SSO, 5 Seats |
| **Trial Days** | Optional free trial period per interval | 14 days |
| **Visibility** | Public (pricing page) or private (internal use) | Public or Private |
Free plans [#free-plans]
A free plan has a price of $0 and no billing cycle. Customers are activated immediately without checkout. Free plans have one restriction: **overage cannot be configured**. Features on a free plan always block usage at the included limit. See [How Do Free Plans Work](/docs/how-do-free-plans-work-without-payment) for details.
Create a plan in the dashboard [#create-a-plan-in-the-dashboard]
Go to **Plans** and click **Create Plan**. Fill in each component from the table above, then save. For detailed feature configuration, see [Configure Features](/docs/configure-features).
{/* TODO: Add screenshot */}
Retrieve plans via SDK [#retrieve-plans-via-sdk]
```typescript
const plans = await commet.plans.list()
```
Include private plans:
```typescript
const plans = await commet.plans.list({ includePrivate: true })
```
Get a specific plan:
```typescript
const plan = await commet.plans.get('plan_xxx')
```
Learn more [#learn-more]
* [How Does Billing Work](/docs/how-does-billing-work)
* [How Do Free Plans Work Without Payment](/docs/how-do-free-plans-work-without-payment)
Related [#related]
* [Consumption Models](/docs/consumption-models) — Metered, Credits, and Balance explained
* [Credit Packs](/docs/credit-packs) — Purchasable credit packages
* [Plan Groups](/docs/plan-groups) — Enable self-service upgrades and downgrades
* [Configure Features](/docs/configure-features) — Boolean, metered, and seat features
* [Manage Subscriptions](/docs/manage-subscriptions) — Assign plans to customers
* [Customer Portal](/docs/customer-portal) — Self-service billing portal
# Credit Packs (/docs/credit-packs)
Credit Packs are additional credit packages customers can purchase when they run out of included plan credits. Only available for plans using the **Credits** [consumption model](/docs/consumption-models).
{/* TODO: Add screenshot */}
Credit pack components [#credit-pack-components]
| Component | Description | Example |
| -------------- | ----------------------------- | ----------------------------------- |
| **Pack Name** | Customer-facing name | "Starter Pack", "Power Pack" |
| **Credits** | Number of credits in the pack | 100, 500, 2000 |
| **Price** | How much the pack costs | $10.00, $40.00 |
| **Visibility** | Which plans can see the pack | All credits plans, or specific ones |
Availability by subscription status [#availability-by-subscription-status]
Credit packs can be purchased on any subscription with a payment method — including during a trial and on free plans. Free plan customers are prompted to enter a payment method on their first purchase.
Create credit packs in the dashboard [#create-credit-packs-in-the-dashboard]
Go to **Credit Packs** and click **Create Credit Pack**. Pack names must be unique within your organization. By default, packs are available to all credits-based plans. You can restrict a pack to specific plans only — useful for offering enterprise-only bulk packs.
{/* TODO: Add screenshot */}
List credit packs via SDK [#list-credit-packs-via-sdk]
```typescript
const packs = await commet.creditPacks.list()
```
**Response:**
```json
{
"success": true,
"data": [
{
"id": "cpk_abc123",
"name": "Starter Pack",
"description": "100 credits for light usage",
"credits": 100,
"price": 1000,
"currency": "usd"
}
]
}
```
The `price` field is in **cents** (1000 = $10.00). Prices are always in USD.
Related [#related]
* [Consumption Models](/docs/consumption-models) — How credits-based billing works
* [Manage Plans](/docs/create-plans) — Create plans with consumption models
* [Customer Portal](/docs/customer-portal) — Where customers purchase credit packs
* [Configure Features](/docs/configure-features) — Define credit costs per feature
# Introductory Offers (/docs/introductory-offers)
An introductory offer is a discount applied automatically to the first N billing cycles for new customers on a plan. Returning customers who have had a paid subscription before pay full price.
Intro offer components [#intro-offer-components]
| Component | Description | Example |
| ------------------ | --------------------------------------------- | ----------------- |
| **Discount Type** | Percentage or fixed amount off the base price | Percentage, Fixed |
| **Discount Value** | How much to discount | 50% off, $20 off |
| **Duration** | Number of billing cycles the discount applies | 3 cycles |
Configure intro offers in the dashboard [#configure-intro-offers-in-the-dashboard]
Navigate to **Plans**, edit a plan, and open the price settings for a billing interval. Set the discount type, value, and duration in cycles. Each plan price has one intro offer configuration.
Each billing interval (monthly, quarterly, yearly) can have its own intro offer. A plan priced at $99/month might offer 50% off for 3 months, while the yearly price has no intro offer at all. Intro offer values are in the same currency as the plan price.
{/* TODO: Add screenshot */}
Regional price overrides [#regional-price-overrides]
Regional prices can have their own intro offer values. A plan offering 50% off in USD might offer 40% off in BRL to account for different price points.
Configure regional intro offers under **Plans** → edit plan → **Regional Prices** → select a currency.
How it works [#how-it-works]
The discount is applied automatically at checkout — no code needed from you or your customer. When a new customer subscribes to a plan with an intro offer, they see the discounted price on the checkout page and pay the reduced amount for the configured number of cycles.
```
Customer subscribes to Pro at $99/mo with "50% off for 3 months":
Month 1: $49.50
Month 2: $49.50
Month 3: $49.50
Month 4: $99.00 (full price)
```
If a customer enters a [Promo Code](/docs/promo-codes) on a plan with an active intro offer, the intro offer takes priority. Promo codes only apply when there is no intro offer for that plan.
100% intro offers [#100-intro-offers]
A 100% intro offer (free first N cycles) is fully supported. When the checkout total is $0, the customer still enters their card for future billing. The subscription activates immediately, and the customer is charged the full plan price when the intro offer cycles expire.
What your customer sees [#what-your-customer-sees]
| Stage | What happens |
| -------- | ------------------------------------------------------------ |
| Checkout | Discounted price displayed with "Introductory offer" label |
| Invoice | Line item shows the discount amount |
| Portal | Remaining intro offer cycles visible in subscription details |
Learn more [#learn-more]
* [How Do Discounts Work](/docs/how-do-discounts-work)
Related [#related]
* [Manage Plans](/docs/create-plans) — Configure plan prices and billing intervals
* [Regional Prices](/docs/regional-prices) — Override intro offer values per currency
* [Promo Codes](/docs/promo-codes) — Marketing discounts customers enter at checkout
* [Trial Periods](/docs/trial-periods) — Free trial before billing begins
# Plan Groups (/docs/plan-groups)
Plan Groups let customers upgrade or downgrade between plans themselves through the [Customer Portal](/docs/customer-portal). Without a Plan Group, customers won't see upgrade/downgrade options in their portal.
Plan group components [#plan-group-components]
| Component | Description | Example |
| -------------- | ----------------------------------------- | ------------------ |
| **Group Name** | Name for the collection | "Standard Plans" |
| **Plans** | Plans in the group, ordered low to high | Free, Starter, Pro |
| **Hierarchy** | Drag-and-drop order defining upgrade path | Lowest tier first |
Create a plan group in the dashboard [#create-a-plan-group-in-the-dashboard]
Go to **Plan Groups** and click **Create Plan Group**. Name the group, add your plans, then drag and drop to arrange them from lowest to highest tier. Each plan can only belong to one group. Plans with [Regional Prices](/docs/regional-prices) cannot be added to a group.
This order defines the upgrade path and display order in the Customer Portal.
{/* TODO: Add screenshot */}
Learn more [#learn-more]
* [What Happens When a Customer Changes Plans](/docs/what-happens-when-a-customer-changes-plans)
Related [#related]
* [Manage Plans](/docs/create-plans) — Create plans to add to groups
* [Upgrade and Downgrade Plans](/docs/upgrade-and-downgrade-plans) — How plan changes work
* [Customer Portal](/docs/customer-portal) — Where customers change plans
# Promo Codes (/docs/promo-codes)
A promo code is a marketing discount code that customers enter manually at checkout to receive a reduced price. Promo codes are managed independently from plans — they are a marketing tool, not a plan setting.
Promo code components [#promo-code-components]
| Component | Description | Example |
| --------------------- | ----------------------------------------------------- | ----------------------------------- |
| **Code** | The string customers type at checkout | `LAUNCH50`, `FRIEND20` |
| **Discount Type** | Percentage or fixed amount | Percentage, Fixed |
| **Discount Value** | How much to discount | 50% off, $20 off |
| **Duration** | How long the discount lasts | Once, Repeating (N cycles), Forever |
| **Max Redemptions** | Total times the code can be used across all customers | 100, unlimited |
| **Expiration Date** | When the code stops working | 2026-12-31 |
| **Plan Restrictions** | Optionally limit to specific plans | Pro only, Pro + Enterprise |
Create a promo code in the dashboard [#create-a-promo-code-in-the-dashboard]
Navigate to **Promo Codes** → **Create**. Enter the code, discount, duration, and optional restrictions. Each code can only be used once per customer.
{/* TODO: Add screenshot */}
Duration types [#duration-types]
* `once`: Discount applies to the first billing cycle only.
* `repeating`: Discount applies for a set number of cycles, then stops.
* `forever`: Discount applies on every billing cycle until the customer changes plans. Changing plans removes any active promo code discount.
Plan restrictions [#plan-restrictions]
By default, a promo code works on any plan. Toggle **Restrict to specific plans** to limit which plans accept the code. If the plan has an active [Introductory Offer](/docs/introductory-offers), the intro offer takes priority and the promo code won't apply for new customers.
How it works [#how-it-works]
Your customer enters the promo code at checkout. The discount preview appears before they pay. The discount must leave a minimum total of $0.50 — codes that would reduce the total below that are rejected.
Promo code discounts apply **only to the plan base price**. Overage, add-on charges, and seat overage are always billed at full price.
```
Customer enters "LAUNCH50" at checkout for Pro at $99/mo:
Subtotal: $99.00
Discount: -$49.50 (LAUNCH50 — 50% off)
Total: $49.50
```
What your customer sees [#what-your-customer-sees]
| Stage | What happens |
| -------- | -------------------------------------------------- |
| Checkout | Code input field, discount preview before payment |
| Invoice | Line item shows the promo code and discount amount |
| Portal | Active discount visible in subscription details |
Learn more [#learn-more]
* [How Do Discounts Work](/docs/how-do-discounts-work)
Related [#related]
* [Introductory Offers](/docs/introductory-offers) — Automatic discounts configured on the plan itself
* [Manage Plans](/docs/create-plans) — Create plans that promo codes apply to
* [Manage Subscriptions](/docs/manage-subscriptions) — Subscribe customers to plans
# Regional Prices (/docs/regional-prices)
Regional Prices let you define local currency pricing for your plans. Customers see and pay in their own currency at checkout, while your canonical prices stay in USD. All regional prices are derived from your USD base prices.
Supported currencies [#supported-currencies]
| Region | Currencies |
| -------------------------- | ------------------------------------------- |
| **Default** | USD |
| **Latin America** | ARS, BRL, CLP, COP, PEN, UYU, PYG, BOB, MXN |
| **North America / Europe** | CAD, EUR |
| **Asia-Pacific** | JPY, CNY, KRW, HKD, SGD, TWD, INR, THB |
What you can configure per currency [#what-you-can-configure-per-currency]
| Component | Description | Example |
| -------------------------- | -------------------------------- | ---------------------- |
| **Base prices** | Price per billing interval | ARS 89,900/month |
| **Feature overage prices** | Cost per extra unit | ARS 9.50 per call |
| **Included balance** | Starting balance (balance model) | ARS 50,000 |
| **Intro offer overrides** | Discount for introductory offers | 20% off first 3 months |
Configure regional prices in the dashboard [#configure-regional-prices-in-the-dashboard]
Navigate to a plan's detail page and click **Regional Prices**. Plans in a [Plan Group](/docs/plan-groups) cannot have regional prices.
Add a currency, enter the exchange rate relative to USD, and Commet auto-calculates all local prices from your USD base prices. To set a custom price for any field, toggle off **Sync rate**. Manual prices won't change when you update the exchange rate.
Once a customer completes their first payment, their subscription currency is locked to whatever currency was resolved at checkout.
{/* TODO: Add screenshot */}
Auto-sync vs manual prices [#auto-sync-vs-manual-prices]
| Mode | Behavior |
| -------------------- | ---------------------------------------------------------------------- |
| **Synced** (default) | Price auto-updates when you change the exchange rate or USD base price |
| **Manual** | Price stays fixed regardless of exchange rate or USD changes |
Related [#related]
* [Manage Plans](/docs/create-plans) — Configure base prices in USD
* [Consumption Models](/docs/consumption-models) — How overage pricing works
* [Manage Subscriptions](/docs/manage-subscriptions) — Currency selection at checkout
# Trial Periods (/docs/trial-periods)
A trial period lets customers use your product before being charged. Configure trial days on a plan and every new subscription gets a trial automatically.
Trial components [#trial-components]
| Component | Description | Example |
| ---------------- | --------------------------------------------------- | --------------------------------- |
| **Trial Days** | Free trial duration before billing | 7, 14, 30 |
| **Per Interval** | Each billing interval can have different trial days | Monthly: 14 days, Yearly: 30 days |
| **Skip Trial** | Bypass trial for specific customers via SDK | `skipTrial: true` |
Configure trial days in the dashboard [#configure-trial-days-in-the-dashboard]
In the dashboard, go to **Plans**, edit a plan, and set **Trial Days** on each price interval. Trial days are configured per plan price, not per customer. Each interval (monthly, quarterly, yearly) can have its own trial duration. Free plans cannot have trials.
{/* TODO: Add screenshot */}
Create a subscription with trial [#create-a-subscription-with-trial]
No code changes needed to enable trials. The subscription starts in `trialing` status automatically when the plan has trial days configured.
```typescript
const { data } = await commet.subscriptions.create({
customerId: 'user_123',
planCode: 'pro',
})
// data.status → 'trialing'
// data.trialEndsAt → '2026-01-15T00:00:00.000Z'
redirect(data.checkoutUrl)
```
Skip a trial [#skip-a-trial]
```typescript
const { data } = await commet.subscriptions.create({
customerId: 'user_123',
planCode: 'pro',
skipTrial: true,
})
```
`skipTrial` is the only way to bypass a plan's trial for a specific customer.
Check trial status [#check-trial-status]
```typescript
const { data } = await commet.subscriptions.get('user_123')
if (data.status === 'trialing') {
const trialEnd = new Date(data.trialEndsAt)
}
```
Learn more [#learn-more]
* [How Do Trial Periods Work](/docs/how-do-trial-periods-work)
Related [#related]
* [Manage Plans](/docs/create-plans) — Configure trial days on plan prices
* [Manage Subscriptions](/docs/manage-subscriptions) — Subscribe customers to plans
* [Handle Failed Payments](/docs/handle-failed-payments) — What happens after trial ends
# Integrate with Django (/docs/integrate-with-django)
Install [#install]
```bash
pip install commet-sdk django
```
```bash
uv add commet-sdk django
```
```bash
poetry add commet-sdk django
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```python title="billing/commet_client.py"
import os
from commet import Commet
commet = Commet(
api_key=os.environ["COMMET_API_KEY"],
environment="sandbox",
)
```
Subscribe [#subscribe]
```python title="billing/views.py"
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .commet_client import commet
@require_POST
def subscribe(request):
data = json.loads(request.body)
commet.customers.create(
email=data["email"],
id=data["customer_id"],
)
subscription = commet.subscriptions.create(
customer_id=data["customer_id"],
plan_code="pro",
)
return JsonResponse({"checkout_url": subscription.data["checkout_url"]})
```
Check Access [#check-access]
```python title="billing/views.py"
def get_subscription(request, customer_id):
sub = commet.subscriptions.get(customer_id)
return JsonResponse({"status": sub.data["status"]})
def check_feature(request, feature, customer_id):
result = commet.features.check(code=feature, customer_id=customer_id)
return JsonResponse({"allowed": result.data["allowed"]})
```
Track Usage [#track-usage]
```python title="billing/views.py"
@require_POST
def track_usage(request):
data = json.loads(request.body)
commet.usage.track(
customer_id=data["customer_id"],
feature="api_calls",
value=1,
)
return JsonResponse({"tracked": True})
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```python title="billing/views.py"
from django.shortcuts import redirect
def portal(request):
result = commet.portal.get_url(customer_id="user_123")
return redirect(result.data["portal_url"])
```
Webhooks [#webhooks]
```python title="billing/views.py"
import os
from django.views.decorators.csrf import csrf_exempt
from commet import Webhooks
webhooks = Webhooks()
@csrf_exempt
@require_POST
def handle_webhook(request):
payload = webhooks.verify_and_parse(
raw_body=request.body.decode(),
signature=request.headers.get("x-commet-signature"),
secret=os.environ["COMMET_WEBHOOK_SECRET"],
)
if payload is None:
return JsonResponse({"error": "Invalid signature"}, status=401)
if payload["event"] == "subscription.activated":
# handle activation
pass
return JsonResponse({"ok": True})
```
URLs [#urls]
```python title="billing/urls.py"
from django.urls import path
from . import views
urlpatterns = [
path("subscribe", views.subscribe),
path("subscription/", views.get_subscription),
path("features//", views.check_feature),
path("usage", views.track_usage),
path("portal", views.portal),
path("webhooks/commet", views.handle_webhook),
]
```
```python title="project/urls.py"
from django.urls import path, include
urlpatterns = [
path("billing/", include("billing.urls")),
]
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with FastAPI (/docs/integrate-with-fastapi)
Install [#install]
```bash
pip install commet-sdk fastapi uvicorn
```
```bash
uv add commet-sdk fastapi uvicorn
```
```bash
poetry add commet-sdk fastapi uvicorn
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```python title="commet_client.py"
import os
from commet import Commet
commet = Commet(
api_key=os.environ["COMMET_API_KEY"],
environment="sandbox",
)
```
Subscribe [#subscribe]
```python title="routes/billing.py"
from fastapi import APIRouter
from pydantic import BaseModel
from commet_client import commet
router = APIRouter(prefix="/billing")
class SubscribeRequest(BaseModel):
customer_id: str
email: str
@router.post("/subscribe")
def subscribe(body: SubscribeRequest):
commet.customers.create(
email=body.email,
id=body.customer_id,
)
subscription = commet.subscriptions.create(
customer_id=body.customer_id,
plan_code="pro",
)
return {"checkout_url": subscription.data["checkout_url"]}
```
Check Access [#check-access]
```python title="routes/billing.py"
@router.get("/subscription/{customer_id}")
def get_subscription(customer_id: str):
sub = commet.subscriptions.get(customer_id)
return {"status": sub.data["status"]}
@router.get("/features/{feature}/{customer_id}")
def check_feature(feature: str, customer_id: str):
result = commet.features.check(code=feature, customer_id=customer_id)
return {"allowed": result.data["allowed"]}
```
Track Usage [#track-usage]
```python title="routes/billing.py"
class UsageRequest(BaseModel):
customer_id: str
@router.post("/usage")
def track_usage(body: UsageRequest):
commet.usage.track(
customer_id=body.customer_id,
feature="api_calls",
value=1,
)
return {"tracked": True}
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```python title="routes/billing.py"
from fastapi.responses import RedirectResponse
@router.get("/portal")
def portal():
result = commet.portal.get_url(customer_id="user_123")
return RedirectResponse(result.data["portal_url"])
```
Webhooks [#webhooks]
```python title="routes/webhooks.py"
import os
from fastapi import APIRouter, Request, Response
from commet import Webhooks
router = APIRouter()
webhooks = Webhooks()
@router.post("/webhooks/commet")
async def handle_webhook(request: Request):
raw_body = await request.body()
payload = webhooks.verify_and_parse(
raw_body=raw_body.decode(),
signature=request.headers.get("x-commet-signature"),
secret=os.environ["COMMET_WEBHOOK_SECRET"],
)
if payload is None:
return Response(status_code=401)
if payload["event"] == "subscription.activated":
# handle activation
pass
return Response(status_code=200)
```
Start Server [#start-server]
```python title="main.py"
from fastapi import FastAPI
from routes.billing import router as billing_router
from routes.webhooks import router as webhooks_router
app = FastAPI()
app.include_router(billing_router)
app.include_router(webhooks_router)
```
```bash
uvicorn main:app --port 3000
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Flask (/docs/integrate-with-flask)
Install [#install]
```bash
pip install commet-sdk flask
```
```bash
uv add commet-sdk flask
```
```bash
poetry add commet-sdk flask
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```python title="commet_client.py"
import os
from commet import Commet
commet = Commet(
api_key=os.environ["COMMET_API_KEY"],
environment="sandbox",
)
```
Subscribe [#subscribe]
```python title="routes/billing.py"
from flask import Blueprint, request, jsonify
from commet_client import commet
billing = Blueprint("billing", __name__)
@billing.route("/subscribe", methods=["POST"])
def subscribe():
data = request.get_json()
commet.customers.create(
email=data["email"],
id=data["customer_id"],
)
subscription = commet.subscriptions.create(
customer_id=data["customer_id"],
plan_code="pro",
)
return jsonify({"checkout_url": subscription.data["checkout_url"]})
```
Check Access [#check-access]
```python title="routes/billing.py"
@billing.route("/subscription/")
def get_subscription(customer_id):
sub = commet.subscriptions.get(customer_id)
return jsonify({"status": sub.data["status"]})
@billing.route("/features//")
def check_feature(feature, customer_id):
result = commet.features.check(code=feature, customer_id=customer_id)
return jsonify({"allowed": result.data["allowed"]})
```
Track Usage [#track-usage]
```python title="routes/billing.py"
@billing.route("/usage", methods=["POST"])
def track_usage():
data = request.get_json()
commet.usage.track(
customer_id=data["customer_id"],
feature="api_calls",
value=1,
)
return jsonify({"tracked": True})
```
Usage is aggregated and billed at end of period.
Customer Portal [#customer-portal]
```python title="routes/billing.py"
@billing.route("/portal")
def portal():
result = commet.portal.get_url(customer_id="user_123")
return redirect(result.data["portal_url"])
```
Webhooks [#webhooks]
```python title="routes/webhooks.py"
import os
from flask import Blueprint, request
from commet import Webhooks
webhooks_bp = Blueprint("webhooks", __name__)
webhooks = Webhooks()
@webhooks_bp.route("/webhooks/commet", methods=["POST"])
def handle_webhook():
payload = webhooks.verify_and_parse(
raw_body=request.get_data(as_text=True),
signature=request.headers.get("x-commet-signature"),
secret=os.environ["COMMET_WEBHOOK_SECRET"],
)
if payload is None:
return "Invalid signature", 401
if payload["event"] == "subscription.activated":
# handle activation
pass
return "", 200
```
Start Server [#start-server]
```python title="app.py"
from flask import Flask
from routes.billing import billing
from routes.webhooks import webhooks_bp
app = Flask(__name__)
app.register_blueprint(billing, url_prefix="/billing")
app.register_blueprint(webhooks_bp)
if __name__ == "__main__":
app.run(port=3000)
```
Related [#related]
* [Subscriptions](/docs/manage-subscriptions)
* [Track Usage](/docs/track-usage)
* [Customer Portal](/docs/customer-portal)
* [SDK Reference](/docs/sdk-reference)
# Integrate with Python (/docs/integrate-with-python)
Install [#install]
```bash
pip install commet-sdk
```
```bash
uv add commet-sdk
```
```bash
poetry add commet-sdk
```
Configure [#configure]
```bash title=".env"
COMMET_API_KEY=ck_sandbox_xxx
```
```python title="commet_client.py"
import os
from commet import Commet
commet = Commet(
api_key=os.environ["COMMET_API_KEY"],
environment="sandbox",
)
```
Create Customer and Subscribe [#create-customer-and-subscribe]
`customers.create` is idempotent — if a customer with the same `id` already exists, it returns the existing record.
```python
response = commet.customers.create(
email="user@example.com",
id="user_123",
)
subscription = commet.subscriptions.create(
customer_id="user_123",
plan_code="pro",
)
checkout_url = subscription.data["checkout_url"]
```
The customer is redirected to checkout to complete payment.
Check Access [#check-access]
```python
sub = commet.subscriptions.get("user_123")
status = sub.data["status"]
access = commet.features.check(code="custom_branding", customer_id="user_123")
allowed = access.data["allowed"]
```
Track Usage [#track-usage]
```python
commet.usage.track(
customer_id="user_123",
feature="api_calls",
value=1,
)
```
Usage is aggregated and billed at end of period.
Customer Context [#customer-context]
Scope all operations to avoid repeating `customer_id`:
```python
customer = commet.customer("user_123")
customer.usage.track("api_calls")
customer.features.check("custom_branding")
customer.seats.add("editor", count=3)
```
Related [#related]
* [Flask](/docs/integrate-with-flask)
* [FastAPI](/docs/integrate-with-fastapi)
* [Django](/docs/integrate-with-django)
* [SDK Reference](/docs/sdk-reference)
# Manage Subscriptions (/docs/manage-subscriptions)
Subscriptions connect a customer to a plan and handle recurring billing automatically. Each customer can have one active subscription at a time. Create one via the SDK or dashboard, and Commet manages checkout, invoicing, and lifecycle transitions.
{/* TODO: Add screenshot */}
Subscription lifecycle [#subscription-lifecycle]
| State | Description | Example |
| ------------------- | ------------------------------- | ----------------------------------------------- |
| **Draft** | Created but not yet activated | Subscription just assigned, no checkout sent |
| **Trialing** | Free trial period active | 14-day trial on the Pro plan |
| **Pending Payment** | Checkout sent, awaiting payment | Customer received checkout link but hasn't paid |
| **Active** | Billing normally | Monthly invoice paid, features enabled |
| **Paused** | Temporarily paused, no billing | Customer requested a break |
| **Past Due** | Payment failed, in grace period | Card declined, retry scheduled |
| **Canceled** | No longer billing | Customer canceled or end-of-period reached |
| **Expired** | Reached scheduled end date | Fixed-term subscription ended |
Dashboard management [#dashboard-management]
From the customer detail page, you can assign a plan, change plans, cancel, or regenerate a checkout link if the original expired. If your plans are in a [Plan Group](/docs/plan-groups), customers can also change plans themselves through the [Customer Portal](/docs/customer-portal).
{/* TODO: Add screenshot */}
Create a subscription [#create-a-subscription]
```typescript
const subscription = await commet.subscriptions.create({
customerId: 'user_123',
planCode: 'pro',
})
redirect(subscription.data.checkoutUrl)
```
Returns a `checkoutUrl` — redirect the user there to complete payment.
Parameters [#parameters]
| Parameter | Type | Description |
| ----------------- | --------- | -------------------------------------------------- |
| `customerId` | `string` | Commet customer ID (`cus_xxx`) or your external ID |
| `planCode` | `string` | Plan code (alternative to `planId`) |
| `planId` | `string` | Plan UUID (alternative to `planCode`) |
| `billingInterval` | `string` | `monthly`, `quarterly`, or `yearly` |
| `initialSeats` | `object` | Seat type codes mapped to quantities |
| `skipTrial` | `boolean` | Skip the plan's trial period |
| `successUrl` | `string` | Redirect URL after successful payment |
Get subscription [#get-subscription]
```typescript
const sub = await commet.subscriptions.get('user_123')
if (sub.data?.status === 'active') {
// User has paid
}
```
Cancel [#cancel]
```typescript
await commet.subscriptions.cancel('sub_xxx')
```
The subscription will remain active until the end of the current billing period. Any pending metered usage will be billed in the final invoice.
Canceled subscriptions cannot be reactivated — create a new one.
Learn more [#learn-more]
* [How Does Billing Work](/docs/how-does-billing-work)
Related [#related]
* [Manage Plans](/docs/create-plans) — Create pricing plans that drive subscriptions
* [Upgrade and Downgrade Plans](/docs/upgrade-and-downgrade-plans) — How plan changes work
* [Trial Periods](/docs/trial-periods) — Configure free trial periods
* [Customer Portal](/docs/customer-portal) — Self-service billing portal for customers
# Upgrade and Downgrade Plans (/docs/upgrade-and-downgrade-plans)
Customers change plans through the [Customer Portal](/docs/customer-portal) or from the dashboard. Commet handles proration, feature transitions, and billing adjustments automatically.
{/* TODO: Add screenshot */}
Plan change behavior [#plan-change-behavior]
| Change | Behavior | Example |
| -------------------------- | ----------------------------------------- | ------------------------- |
| **Upgrade** | Applied immediately with prorated billing | Starter ($29) → Pro ($99) |
| **Downgrade** | Takes effect at next renewal | Pro ($99) → Starter ($29) |
| **Interval change (up)** | Applied immediately | Monthly → Yearly |
| **Interval change (down)** | Takes effect at next renewal | Yearly → Monthly |
Both plans must be in the same [Plan Group](/docs/plan-groups) for customers to change plans themselves through the portal. Free-to-paid changes require a new checkout — the customer is redirected to complete payment.
Dashboard [#dashboard]
From a customer's subscription detail page, click **Change Plan** and select the new plan. The same upgrade/downgrade rules apply.
{/* TODO: Add screenshot */}
Learn more [#learn-more]
* [What Happens When a Customer Changes Plans](/docs/what-happens-when-a-customer-changes-plans)
* [How Is Proration Calculated](/docs/how-is-proration-calculated-when-changing-plans)
Related [#related]
* [Manage Subscriptions](/docs/manage-subscriptions) — Create, get, and cancel subscriptions
* [Plan Groups](/docs/plan-groups) — Group plans together for self-service upgrades
* [Customer Portal](/docs/customer-portal) — Self-service billing portal for customers
# AI Token Billing (/docs/ai-token-billing)
AI Token Billing lets you charge customers based on the actual AI model tokens they consume. Commet maintains a catalog of 180+ AI model prices, calculates the cost per request, applies your margin, and deducts from the customer's balance. Only available for plans using the **Balance** [consumption model](/docs/consumption-models).
How it works [#how-it-works]
1. Your app calls an AI model (GPT-4o, Claude, Gemini, etc.)
2. You report the tokens consumed to Commet
3. Commet looks up the model's token price, applies your margin, and deducts from the customer's balance
4. At the end of the billing period, any overdraft is invoiced as overage
Setup [#setup]
1\. Create a feature [#1-create-a-feature]
Go to **Features**, create a metered feature (e.g., name: "AI Chat", code: `ai_chat`).
2\. Add to a balance plan with AI Model pricing [#2-add-to-a-balance-plan-with-ai-model-pricing]
Go to **Plans**, open a plan with Balance consumption model, and add the feature. Select **AI Model** as the pricing mode and set your margin percentage.
| Setting | Description |
| ---------------- | ------------------------------------------------ |
| **Pricing Mode** | Choose "AI Model" instead of "Fixed Price" |
| **Margin** | Your markup on top of the model cost (e.g., 20%) |
3\. Track usage [#3-track-usage]
Pass the `model`, `inputTokens`, and `outputTokens` when tracking. Commet handles the rest.
Track AI tokens with the SDK [#track-ai-tokens-with-the-sdk]
Use the same `track()` method. When you pass `model`, Commet switches to AI token pricing.
```typescript
await commet.usage.track({
customerId: "user_123",
feature: "ai_chat",
model: "gpt-4o",
inputTokens: 1500,
outputTokens: 300,
})
```
For models with prompt caching, include cache tokens for accurate billing:
```typescript
await commet.usage.track({
customerId: "user_123",
feature: "ai_chat",
model: "anthropic/claude-sonnet-4.6",
inputTokens: 10000,
outputTokens: 2000,
cacheReadTokens: 7000,
cacheWriteTokens: 1000,
})
```
Cache read tokens are significantly cheaper than regular input tokens. Commet prices each token type separately so customers pay fair rates.
Automatic tracking with `@commet/ai-sdk` [#automatic-tracking-with-commetai-sdk]
If you use the [Vercel AI SDK](https://ai-sdk.dev), install `@commet/ai-sdk` to track tokens automatically.
```bash
npm install @commet/ai-sdk
```
Wrap your model with `tracked()`. Every `generateText` and `streamText` call is tracked without extra code.
```typescript
import { tracked } from "@commet/ai-sdk"
import { Commet } from "@commet/node"
import { openai } from "@ai-sdk/openai"
import { generateText } from "ai"
const commet = new Commet({ apiKey: process.env.COMMET_API_KEY! })
const model = tracked(openai("gpt-4o"), {
commet,
feature: "ai_chat",
customerId: "user_123",
})
const result = await generateText({ model, prompt: "Hello!" })
// Tokens tracked and balance deducted automatically
```
Works with any AI SDK provider: OpenAI, Anthropic, Google, and any model available through the Vercel AI Gateway.
Parameters [#parameters]
| Parameter | Type | Required | Description |
| ------------------ | -------- | -------- | ------------------------------------------------------------------- |
| `feature` | `string` | Yes | Event code of a metered feature |
| `customerId` | `string` | Yes | Commet customer ID (`cus_xxx`) or your external ID |
| `model` | `string` | Yes | AI model identifier (e.g., `gpt-4o`, `anthropic/claude-sonnet-4.6`) |
| `inputTokens` | `number` | Yes | Number of input (prompt) tokens |
| `outputTokens` | `number` | Yes | Number of output (completion) tokens |
| `cacheReadTokens` | `number` | No | Cached input tokens read (cheaper rate) |
| `cacheWriteTokens` | `number` | No | Cached input tokens written (higher rate) |
| `idempotencyKey` | `string` | No | Prevents duplicate events |
Model identifier formats [#model-identifier-formats]
Commet accepts model identifiers in two formats:
| Format | Example | When to use |
| -------------- | ----------------------------- | ----------------------------------- |
| Model ID only | `gpt-4o` | Direct provider SDK usage |
| Provider/Model | `anthropic/claude-sonnet-4.6` | AI Gateway or multi-provider setups |
Cost calculation [#cost-calculation]
For each request, Commet calculates:
```
inputCost = nonCachedInputTokens x inputPrice / 1M
outputCost = outputTokens x outputPrice / 1M
cacheCost = cacheReadTokens x cachePrice / 1M
+ cacheWriteTokens x cacheWritePrice / 1M
subtotal = inputCost + outputCost + cacheCost
total = subtotal x (1 + margin%)
```
AI model catalog [#ai-model-catalog]
Commet maintains a catalog of 180+ AI models with up-to-date token prices, synchronized daily from the Vercel AI Gateway. The catalog includes input, output, cache read, and cache write prices for each model.
Supported providers include OpenAI, Anthropic, Google, Meta, Mistral, Cohere, and more.
AI Costs dashboard [#ai-costs-dashboard]
View all AI token costs in the dashboard under **AI Costs**. Each entry shows the model used, token breakdown, cost calculation, margin applied, and total charged.
Related [#related]
* [Consumption Models](/docs/consumption-models) — How Balance model works
* [Track Usage](/docs/track-usage) — Standard usage tracking
* [Configure Features](/docs/configure-features) — Create metered features
# Seat Management (/docs/seat-management)
Seats are per-user licenses that let you charge based on team size. Commet tracks seat changes and bills them automatically — included seats at the start of the period, additional seats prorated.
{/* TODO: Add screenshot */}
Seat components [#seat-components]
| Component | Description | Example |
| ------------- | -------------------------------------- | --------------------------- |
| **Seat Type** | Category of user license | `editor`, `admin`, `viewer` |
| **Count** | Number of seats to add, remove, or set | `5`, `10`, `50` |
| **Billing** | Seats are billed per unit on the plan | $25/seat/month |
Dashboard [#dashboard]
Create seat types from **Seats**, then **Types**, then **Add seat type**. Seat types must be created in the dashboard before use. View current seat balances on each customer's subscription detail page.
{/* TODO: Add screenshot */}
Add seats [#add-seats]
```typescript
await commet.seats.add({
customerId: 'user_123',
seatType: 'editor',
count: 5,
})
```
Remove seats [#remove-seats]
```typescript
await commet.seats.remove({
customerId: 'user_123',
seatType: 'editor',
count: 2,
})
```
Seats cannot go below zero — removing more than the current count will fail.
Set exact count [#set-exact-count]
Use `set` when syncing seat counts from your system.
```typescript
await commet.seats.set({
customerId: 'user_123',
seatType: 'editor',
count: 10,
})
```
Get balance [#get-balance]
```typescript
const balance = await commet.seats.getBalance({
customerId: 'user_123',
seatType: 'editor',
})
```
Pass either a Commet ID (`cus_xxx`) or your external ID as `customerId`. One active subscription per customer is required.
Learn more [#learn-more]
* [How Does Seat-Based Billing Work](/docs/how-does-seat-based-billing-work)
Related [#related]
* [Configure Features](/docs/configure-features) — Create seat features on your plans
* [Manage Plans](/docs/create-plans) — Plans that include seat-based pricing
* [Manage Subscriptions](/docs/manage-subscriptions) — Assign plans with initial seats
# Track Usage (/docs/track-usage)
Every metered feature has an event code. Your application sends events with that code, and Commet aggregates them for billing at the end of the period.
{/* TODO: Add screenshot */}
Usage event components [#usage-event-components]
| Component | Description | Example |
| ------------------- | ---------------------------------- | ------------------------- |
| **Feature** | Event code of a metered feature | `api_calls`, `storage_gb` |
| **Value** | Quantity consumed | `1`, `0.5`, `100` |
| **Customer** | Who consumed it | `customerId: "user_123"` |
| **Idempotency Key** | Prevents duplicate events | `"req_abc123"` |
| **Timestamp** | When it happened (defaults to now) | `"2026-01-15T10:00:00Z"` |
Setting up event codes [#setting-up-event-codes]
When you create a metered feature in the dashboard, you define its event code. Go to **Features**, click **Create Feature**, select **Metered**, and enter the event code.
{/* TODO: Add screenshot */}
Event code conventions [#event-code-conventions]
| Good Event Codes | Bad Event Codes |
| ----------------- | --------------- |
| `api_calls` | `feature1` |
| `storage_gb` | `metric` |
| `emails_sent` | `usage` |
| `compute_minutes` | `x` |
Use snake\_case. Be descriptive — this is what appears in code.
Track a single event [#track-a-single-event]
```typescript
await commet.usage.track({
customerId: "user_123",
feature: "api_calls",
value: 1,
})
```
Pass either a Commet ID (`cus_xxx`) or your external ID as `customerId`.
Track in batches [#track-in-batches]
More efficient for high-volume tracking. Maximum **100 events** per batch request.
```typescript
await commet.usage.trackBatch({
events: [
{ customerId: "user_123", feature: "api_calls", value: 1 },
{ customerId: "user_456", feature: "api_calls", value: 3 },
{ customerId: "user_123", feature: "storage_gb", value: 0.5 },
],
})
```
Parameters [#parameters]
| Parameter | Type | Required | Description |
| ---------------- | -------- | -------- | --------------------------------------------------------------------------------------------- |
| `feature` | `string` | Yes | Event code of a metered feature. Only lowercase letters, numbers, and underscores (`a-z0-9_`) |
| `customerId` | `string` | Yes | Commet customer ID (`cus_xxx`) or your external ID |
| `value` | `number` | No | Quantity consumed. Must be >= 0. Defaults to `1` |
| `idempotencyKey` | `string` | No | Prevents duplicate events |
| `timestamp` | `string` | No | ISO 8601 datetime. Defaults to now |
| `properties` | `array` | No | Key-value metadata for debugging |
Idempotency [#idempotency]
Prevent duplicate charges by passing an `idempotencyKey`. Commet rejects events with a key that was already recorded for the same customer.
```typescript
await commet.usage.track({
customerId: "user_123",
feature: "api_calls",
value: 1,
idempotencyKey: "req_abc123",
})
```
Related [#related]
* [AI Token Billing](/docs/ai-token-billing) — Automatically track and charge for AI model tokens
* [Configure Features](/docs/configure-features) — Create metered features and event codes
* [Consumption Models](/docs/consumption-models) — How usage is billed across metered, credits, and balance