| name | implementing-autumn-billing |
| description | Implement Autumn billing and payments in applications. Use when building subscription systems, usage-based billing, paywalls, feature gates, or monetization features. Autumn is an open-source billing layer between your app and Stripe. |
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
bun add @useautumn/convex autumn-js
For Next.js / Express Projects
npm install autumn-js
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";
const messages = feature({
id: "messages",
name: "Messages",
type: "single_use",
});
const workspaces = feature({
id: "workspaces",
name: "Workspaces",
type: "single_use",
});
const free = product({
id: "free",
name: "Free",
is_default: true,
items: [
featureItem({
feature_id: messages.id,
included_usage: 50,
interval: "month",
}),
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) => {
const user = await ctx.auth.getUserIdentity();
if (!user) return null;
return {
customerId: user.subject,
customerData: {
name: user.name as string,
email: user.email as string,
},
};
},
});
export const {
track,
cancel,
query,
attach,
check,
checkout,
usage,
setupPayment,
createCustomer,
listProducts,
billingPortal,
createReferralCode,
redeemReferralCode,
createEntity,
getEntity,
} = autumn.api();
Next.js / Express Setup
import { Autumn } from "autumn-js";
export const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY!,
});
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 });
}
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:
const result = await autumn.check(ctx, {
featureId: "messages",
requiredBalance: 1,
});
if (result.allowed) {
} else {
}
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:
await autumn.track(ctx, {
featureId: "api_calls",
value: 1,
});
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",
}),
});
Initiate Checkout
Start a checkout session for upgrades:
const result = await autumn.checkout(ctx, {
productId: "pro",
});
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, {});
Frontend Integration
React Provider (Web)
"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,
});
};
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) {
const result = await autumn.check(ctx, {
featureId: "messages",
});
if (!result.allowed) {
throw new Error("BILLING_LIMIT_EXCEEDED");
}
const message = await createMessage(args);
await autumn.track(ctx, {
featureId: "messages",
value: 1,
});
return message;
}
Entity-Based Billing (Per-Seat)
await autumn.track(ctx, {
featureId: "seats",
entityId: teamMemberId,
value: 1,
entityData: {
name: memberName,
email: memberEmail,
},
});
const result = await autumn.check(ctx, {
featureId: "seats",
entityId: teamMemberId,
});
Credit System
await autumn.attach(ctx, {
featureId: "credits",
value: 100,
});
await autumn.track(ctx, {
featureId: "credits",
value: 10,
});
const result = await autumn.check(ctx, {
featureId: "credits",
});
console.log(result.balance.current_balance);
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;
}
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 }),
}));
Resources