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.
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.
npm install @useautumn/convex autumn-js
# or
bun add @useautumn/convex autumn-js
npm install autumn-js
# or
bun add autumn-js
Add to your .env or environment variables:
AUTUMN_SECRET_KEY=am_sk_your_secret_key
Get your secret key from https://app.useautumn.com
Autumn supports defining features and products in code using autumn.config.ts:
npx atmn init
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],
};
npx atmn push
| 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 |
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;
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();
// 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 });
}
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"
}
}
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
}),
});
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",
}),
});
Open Stripe billing portal for subscription management:
const result = await autumn.billingPortal(ctx, {});
// Returns { url: "https://billing.stripe.com/..." }
// 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>
);
}
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>;
}
import { PricingTable } from "autumn-js/react";
function PricingPage() {
return <PricingTable />;
}
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 |
Define your pricing tiers:
Free Plan:
Pro Plan ($19/mo):
Enterprise (Custom):
Link your Stripe account in the Autumn dashboard to enable payment processing.
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;
}
// 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,
});
// 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
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)} />}
</>
);
}
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.
Use test API keys (prefix am_sk_test_) for development:
AUTUMN_SECRET_KEY=am_sk_test_xxx
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 }),
}));