Implementing Autumn Billing
Autumn is an open-source billing management system that sits between your application and Stripe. It handles subscription state, usage metering, credit balances, and feature gating without requiring webhook management.
When to Use This Skill
- •Adding subscription/payment features to an app
- •Implementing usage-based billing (metered features)
- •Creating paywalls or feature gates
- •Building credit systems
- •Managing subscription tiers (Free/Pro/Enterprise)
- •Implementing per-seat or per-entity pricing
Installation
For Convex Projects
npm install @useautumn/convex autumn-js # or bun add @useautumn/convex autumn-js
For Next.js / Express Projects
npm install autumn-js # or bun add autumn-js
Environment Setup
Add to your .env or environment variables:
AUTUMN_SECRET_KEY=am_sk_your_secret_key
Get your secret key from https://app.useautumn.com
Pricing as Code (CLI)
Autumn supports defining features and products in code using autumn.config.ts:
1. Initialize Project
npx atmn init
2. Define Features and Products
Create autumn.config.ts:
import {
feature,
product,
featureItem,
priceItem,
} from "atmn";
// Features
const messages = feature({
id: "messages",
name: "Messages",
type: "single_use", // Consumed on each use
});
const workspaces = feature({
id: "workspaces",
name: "Workspaces",
type: "single_use",
});
// Products
const free = product({
id: "free",
name: "Free",
is_default: true,
items: [
featureItem({
feature_id: messages.id,
included_usage: 50,
interval: "month", // Resets monthly
}),
featureItem({
feature_id: workspaces.id,
included_usage: 2,
}),
],
});
const pro = product({
id: "pro",
name: "Pro",
items: [
priceItem({
price: 19,
interval: "month",
}),
featureItem({
feature_id: messages.id,
included_usage: 500,
interval: "month",
}),
featureItem({
feature_id: workspaces.id,
included_usage: 10,
}),
],
});
const enterprise = product({
id: "enterprise",
name: "Enterprise",
items: [
featureItem({
feature_id: messages.id,
unlimited: true,
}),
featureItem({
feature_id: workspaces.id,
unlimited: true,
}),
],
});
export default {
features: [messages, workspaces],
products: [free, pro, enterprise],
};
3. Push to Autumn
npx atmn push
CLI Commands
| Command | Description |
|---|---|
npx atmn init | Initialize project, pull existing config |
npx atmn push | Push local config to Autumn |
npx atmn pull | Pull remote config to local |
npx atmn auth | Authenticate with Autumn |
npx atmn dashboard | Open Autumn dashboard |
Backend Integration
Convex Setup
- •Register the component in
convex/convex.config.ts:
import { defineApp } from "convex/server";
import autumn from "@useautumn/convex/convex.config";
const app = defineApp();
app.use(autumn);
export default app;
- •Initialize the client in
convex/autumn.ts:
import { components } from "./_generated/api";
import { Autumn } from "@useautumn/convex";
export const autumn = new Autumn(components.autumn, {
secretKey: process.env.AUTUMN_SECRET_KEY ?? "",
identify: async (ctx: any) => {
// Customize based on your auth provider
const user = await ctx.auth.getUserIdentity();
if (!user) return null;
return {
customerId: user.subject, // Unique user ID
customerData: {
name: user.name as string,
email: user.email as string,
},
};
},
});
// Export API functions
export const {
track,
cancel,
query,
attach,
check,
checkout,
usage,
setupPayment,
createCustomer,
listProducts,
billingPortal,
createReferralCode,
redeemReferralCode,
createEntity,
getEntity,
} = autumn.api();
Next.js / Express Setup
// lib/autumn.ts
import { Autumn } from "autumn-js";
export const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY!,
});
// In API routes
export async function POST(req: Request) {
const { userId } = await getAuth(req);
const result = await autumn.check({
customerId: userId,
featureId: "api_calls",
});
if (!result.allowed) {
return Response.json({ error: "Limit exceeded" }, { status: 403 });
}
// Process request...
await autumn.track({
customerId: userId,
featureId: "api_calls",
value: 1,
});
return Response.json({ success: true });
}
Core API Reference
Check Feature Access
Check if a customer can access a feature:
// Convex
const result = await autumn.check(ctx, {
featureId: "messages",
requiredBalance: 1, // Optional: minimum balance needed
});
if (result.allowed) {
// Allow action
} else {
// Show upgrade prompt
}
// Direct API
const response = await fetch("https://api.useautumn.com/v1/check", {
method: "POST",
headers: {
"Authorization": `Bearer ${AUTUMN_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_id: userId,
feature_id: "messages",
required_balance: 1,
}),
});
Response:
{
"allowed": true,
"customer_id": "user_123",
"balance": {
"feature_id": "messages",
"unlimited": false,
"granted_balance": 100,
"purchased_balance": 0,
"current_balance": 75,
"usage": 25,
"overage_allowed": false,
"reset": {
"interval": "month",
"interval_count": 1,
"resets_at": 1735689600000
},
"plan_id": "pro"
}
}
Track Usage
Record usage for metered features:
// Convex
await autumn.track(ctx, {
featureId: "api_calls",
value: 1,
});
// Direct API
await fetch("https://api.useautumn.com/v1/track", {
method: "POST",
headers: {
"Authorization": `Bearer ${AUTUMN_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_id: userId,
feature_id: "api_calls",
value: 1,
idempotency_key: "unique_request_id", // Optional: prevent duplicate tracking
}),
});
Initiate Checkout
Start a checkout session for upgrades:
// Convex
const result = await autumn.checkout(ctx, {
productId: "pro",
});
// Returns { url: "https://checkout.stripe.com/..." }
// Direct API
const response = await fetch("https://api.useautumn.com/v1/checkout", {
method: "POST",
headers: {
"Authorization": `Bearer ${AUTUMN_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_id: userId,
product_id: "pro",
success_url: "https://yourapp.com/success",
cancel_url: "https://yourapp.com/pricing",
}),
});
Billing Portal
Open Stripe billing portal for subscription management:
const result = await autumn.billingPortal(ctx, {});
// Returns { url: "https://billing.stripe.com/..." }
Frontend Integration
React Provider (Web)
// providers/autumn-provider.tsx
"use client";
import { AutumnProvider } from "autumn-js/react";
import { api } from "../convex/_generated/api";
import { useConvex } from "convex/react";
export function AutumnWrapper({ children }: { children: React.ReactNode }) {
const convex = useConvex();
return (
<AutumnProvider convex={convex} convexApi={(api as any).autumn}>
{children}
</AutumnProvider>
);
}
Using the Hook
import { useCustomer, CheckoutDialog } from "autumn-js/react";
function UpgradeButton() {
const { customer, checkout, check } = useCustomer();
const handleUpgrade = async () => {
await checkout({
productId: "pro",
dialog: CheckoutDialog, // Optional: built-in dialog
});
};
return <button onClick={handleUpgrade}>Upgrade to Pro</button>;
}
Pricing Table Component
import { PricingTable } from "autumn-js/react";
function PricingPage() {
return <PricingTable />;
}
Autumn Dashboard Configuration
1. Create Features
In the Autumn dashboard, define your metered features:
| Feature ID | Type | Reset Interval |
|---|---|---|
messages | Metered | Monthly |
workspaces | Metered | None (cumulative) |
api_calls | Metered | Monthly |
storage_gb | Metered | None |
2. Create Products/Plans
Define your pricing tiers:
Free Plan:
- •messages: 50/month
- •workspaces: 2
Pro Plan ($19/mo):
- •messages: 500/month
- •workspaces: 10
Enterprise (Custom):
- •messages: Unlimited
- •workspaces: Unlimited
3. Connect Stripe
Link your Stripe account in the Autumn dashboard to enable payment processing.
Common Patterns
Paywall Before Action
async function sendMessage(ctx, args) {
// 1. Check entitlement
const result = await autumn.check(ctx, {
featureId: "messages",
});
if (!result.allowed) {
throw new Error("BILLING_LIMIT_EXCEEDED");
}
// 2. Perform action
const message = await createMessage(args);
// 3. Track usage
await autumn.track(ctx, {
featureId: "messages",
value: 1,
});
return message;
}
Entity-Based Billing (Per-Seat)
// Track usage per entity (e.g., per team member)
await autumn.track(ctx, {
featureId: "seats",
entityId: teamMemberId,
value: 1,
entityData: {
name: memberName,
email: memberEmail,
},
});
// Check entity-specific access
const result = await autumn.check(ctx, {
featureId: "seats",
entityId: teamMemberId,
});
Credit System
// Grant credits
await autumn.attach(ctx, {
featureId: "credits",
value: 100,
});
// Use credits
await autumn.track(ctx, {
featureId: "credits",
value: 10,
});
// Check remaining
const result = await autumn.check(ctx, {
featureId: "credits",
});
console.log(result.balance.current_balance); // 90
Handling Billing Errors in UI
function ChatInput() {
const [error, setError] = useState(null);
const [showUpgrade, setShowUpgrade] = useState(false);
const handleSend = async (message) => {
try {
await sendMessage({ content: message });
} catch (err) {
if (err.message.includes("BILLING_LIMIT_EXCEEDED")) {
setShowUpgrade(true);
} else {
setError(err.message);
}
}
};
return (
<>
<Input onSubmit={handleSend} />
{showUpgrade && <UpgradeModal onClose={() => setShowUpgrade(false)} />}
</>
);
}
Error Handling Best Practices
- •Fail open on API errors: If Autumn API is unavailable, allow the action rather than blocking users.
try {
const result = await autumn.check(ctx, { featureId: "messages" });
if (!result.allowed) {
throw new Error("BILLING_LIMIT_EXCEEDED");
}
} catch (error) {
if (error.message === "BILLING_LIMIT_EXCEEDED") {
throw error; // Re-throw billing errors
}
// Log but continue on API errors
console.error("[Autumn] Check failed:", error);
}
- •
Use idempotency keys for tracking to prevent duplicate charges.
- •
Track after success, not before, to avoid charging for failed operations.
Testing
Test Mode
Use test API keys (prefix am_sk_test_) for development:
AUTUMN_SECRET_KEY=am_sk_test_xxx
Mock Responses
For unit tests, mock the Autumn API responses:
jest.mock("autumn-js", () => ({
check: jest.fn().mockResolvedValue({ allowed: true, balance: { current_balance: 50 } }),
track: jest.fn().mockResolvedValue({ success: true }),
}));