Add changelog entry for checkout_session_params deep-merge fix (#999)

* docs improvements

* many changes

* prompt changes

* add refetch comment

* fix: checkout subscription data

* cus eligibility fixes

* rm cursor thing

---------

Co-authored-by: Ayush Rodrigues <joesj2905@gmail.com>
Co-authored-by: John Yeo <johnyeocx@gmail.com>
Co-authored-by: John Yeo <51376134+johnyeocx@users.noreply.github.com>
Co-authored-by: Ayush <74830628+ay-rod@users.noreply.github.com>
This commit is contained in:
mintlify[bot]
2026-03-17 17:33:52 +00:00
committed by GitHub
parent ef8367c4af
commit 9bd20cb2e9
48 changed files with 2449 additions and 4486 deletions

View File

@@ -49,16 +49,8 @@
"groups": [
{
"group": " ",
"pages": ["welcome", "documentation/getting-started/migration"]
},
{
"group": "Getting Started",
"pages": [
"documentation/getting-started/setup",
"documentation/getting-started/gating",
"documentation/getting-started/display-billing"
]
},
"welcome",
{
"group": "Concepts",
"pages": [
@@ -71,20 +63,32 @@
"documentation/concepts/stripe"
]
},
"documentation/getting-started/migration"
]
},
{
"group": "Modelling Pricing",
"group": "Getting Started",
"pages": [
"documentation/getting-started/setup",
"documentation/getting-started/gating",
"documentation/getting-started/display-billing"
]
},
{
"group": "Configure Pricing",
"pages": [
"documentation/modelling-pricing/recurring",
"documentation/modelling-pricing/one-off-purchases",
"documentation/modelling-pricing/free-plans",
"documentation/modelling-pricing/trials",
"documentation/modelling-pricing/credit-systems",
"documentation/modelling-pricing/per-unit-pricing",
"documentation/modelling-pricing/prepaid-pricing",
"documentation/modelling-pricing/usage-based-pricing",
"documentation/modelling-pricing/per-unit-pricing",
"documentation/modelling-pricing/rollovers",
"documentation/modelling-pricing/proration",
"documentation/modelling-pricing/spend-limits",
"documentation/modelling-pricing/trials",
"documentation/modelling-pricing/auto-top-ups",
"documentation/modelling-pricing/spend-limits",
"documentation/modelling-pricing/add-ons",
"documentation/modelling-pricing/graduated-pricing",
"documentation/modelling-pricing/volume-based-tiers",
@@ -100,7 +104,8 @@
"documentation/customers/subscription-lifecycle",
"documentation/customers/updating-subscriptions",
"documentation/customers/custom-plans",
"documentation/customers/versioning"
"documentation/customers/versioning",
"documentation/customers/edge-cases"
]
},
{
@@ -118,7 +123,6 @@
{
"group": "Additional Resources",
"pages": [
"documentation/edge-cases",
"documentation/webhooks",
"documentation/external-providers/convex",
"documentation/external-providers/revenuecat",

View File

@@ -1,5 +1,5 @@
---
title: "Edge Cases"
title: "Billing Reliability"
description: "How Autumn handles 3DS, payment failures, and other uncommon states"
---

View File

@@ -7,138 +7,41 @@ Software applications typically ship with a billing page. This allows customers
The customer endpoint returns the current state of the customer, including their active subscriptions, one-time purchases, and feature balances.
### Active plans
## Pricing table
Display the plan the user is currently on. Users can have multiple active subscriptions and purchases (e.g., main plan and add-ons).
- **`subscriptions`** - Free and paid recurring plans
- **`purchases`** - One-off plans (e.g., credit top-ups)
<CodeGroup>
```jsx React
import { useCustomer } from "autumn-js/react";
const { data: customer } = useCustomer();
const active = customer?.subscriptions.filter(
(sub) => sub.status === "active"
);
console.log(active?.map((sub) => sub.planId).join(", "));
```
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const active = customer.subscriptions?.filter(
(sub) => sub.status === "active"
);
console.log(active?.map((sub) => sub.planId).join(", "));
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_1234")
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
active = [s for s in customer.subscriptions if s.status == "active"]
print([s.plan_id for s in active])
```
```bash cURL
curl -X POST 'https://api.useautumn.com/v1/customers' \
-H 'Authorization: Bearer am_sk_test_1234' \
-H 'Content-Type: application/json' \
-d '{ "customer_id": "user_123" }'
# Response includes subscriptions array
```
</CodeGroup>
### Usage balances
Metered features have `granted`, `usage`, and `remaining` fields. Use these to display current usage and remaining balance.
<CodeGroup>
```jsx React
import { useCustomer } from "autumn-js/react";
const { data: customer } = useCustomer();
const messages = customer?.balances.messages;
console.log(`${messages?.remaining} / ${messages?.granted}`);
```
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const messages = customer.balances?.messages;
console.log(`${messages?.remaining} / ${messages?.granted}`);
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_1234")
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
messages = customer.balances.get("messages")
print(f"{messages.remaining} / {messages.granted}")
```
```bash cURL
curl -X POST 'https://api.useautumn.com/v1/customers' \
-H 'Authorization: Bearer am_sk_test_1234' \
-H 'Content-Type: application/json' \
-d '{ "customer_id": "user_123" }'
# Response includes balances.[feature_id]
```
</CodeGroup>
## Billing flows
### Customer eligibility
When building a pricing page, you need to know what each plan means for the current customer — is it an upgrade, a downgrade, or their current plan? Is a free trial available?
When building a pricing table, you need to know what each plan means for the current customer — is it an upgrade, a downgrade, or their current plan? Is a free trial available?
Pass a `customerId` when listing plans and each plan will include a `customerEligibility` object:
- **`scenario`** — The attach scenario for this customer: `new`, `upgrade`, `downgrade`, `active`, `scheduled`, `cancel`, `expired`, `past_due`, or `renew`
- **`trialAvailable`** — Whether the customer is eligible for the plan's free trial
| Field | Type | Description |
|-------|------|-------------|
| `attachAction` | `"activate"` \| `"upgrade"` \| `"downgrade"` \| `"purchase"` \| `"none"` | What happens when this plan is attached |
| `status` | `"active"` \| `"scheduled"` \| undefined | The customer's current relationship to this plan |
| `trialAvailable` | boolean | Whether the customer is eligible for the plan's free trial |
<CodeGroup>
```jsx React
```jsx React expandable
import { useListPlans, useCustomer } from "autumn-js/react";
const buttonText = {
new: "Get started",
const labels = {
activate: "Subscribe",
upgrade: "Upgrade",
downgrade: "Downgrade",
active: "Current plan",
purchase: "Purchase",
};
const getLabel = (eligibility) => {
if (eligibility?.attachAction === "none") {
return eligibility.status === "scheduled" ? "Plan Scheduled" : "Current plan";
}
if (labels[eligibility?.attachAction]) {
return labels[eligibility.attachAction];
}
return "Get started";
};
export default function PricingPage() {
@@ -148,10 +51,10 @@ export default function PricingPage() {
return plans?.map((plan) => (
<button
key={plan.id}
disabled={plan.customerEligibility?.scenario === "active"}
onClick={() => attach({ planId: plan.id })}
disabled={plan.customerEligibility?.attachAction === "none"}
onClick={() => attach({ planId: plan.id, redirectMode: "always" })}
>
{buttonText[plan.customerEligibility?.scenario] ?? "Get started"}
{getLabel(plan.customerEligibility)}
</button>
));
}
@@ -166,10 +69,9 @@ const { list: plans } = await autumn.plans.list({
customerId: "user_123",
});
// Each plan includes customerEligibility.scenario and customerEligibility.trialAvailable
for (const plan of plans) {
console.log(plan.name, plan.customerEligibility?.scenario);
// e.g. "Free" "downgrade", "Pro" "active", "Enterprise" "upgrade"
console.log(plan.name, plan.customerEligibility?.attachAction);
// e.g. "Free" "downgrade", "Pro" "none", "Enterprise" "upgrade"
}
```
@@ -180,9 +82,8 @@ autumn = Autumn("am_sk_test_1234")
plans = await autumn.plans.list(customer_id="user_123")
# Each plan includes customer_eligibility.scenario and customer_eligibility.trial_available
for plan in plans.list:
print(plan.name, plan.customer_eligibility.scenario)
print(plan.name, plan.customer_eligibility.attach_action)
```
```bash cURL
@@ -200,7 +101,7 @@ curl -X POST 'https://api.useautumn.com/v1/plans.list' \
The React `useListPlans` hook automatically includes customer context from `AutumnProvider`, so `customerEligibility` is populated on every plan without extra configuration.
</Tip>
### Switching plans
## Switching plans
Switching plans uses `billing.attach`. See [Attaching Plans](/documentation/customers/payment-flow) for the full guide.
@@ -258,7 +159,7 @@ curl -X POST 'https://api.useautumn.com/v1/attach' \
</CodeGroup>
### Cancelling a plan
## Cancelling a plan
Cancel a subscription using `billing.update` with a `cancelAction`. See [Subscription Lifecycle](/documentation/customers/subscription-lifecycle#cancellations) for the full guide on immediate vs end-of-cycle cancellations.
@@ -315,7 +216,7 @@ curl -X POST 'https://api.useautumn.com/v1/billing/update' \
</CodeGroup>
### Uncancelling a plan
## Uncancelling a plan
If a subscription has a pending cancellation, a scheduled downgrade, or a scheduled plan switch, you can reverse it with `cancelAction: "uncancel"`.
@@ -402,6 +303,122 @@ curl -X POST 'https://api.useautumn.com/v1/billing/update' \
</CodeGroup>
## Active plans
Display the plan the user is currently on. Users can have multiple active subscriptions and purchases (e.g., main plan and add-ons).
- **`subscriptions`** - Free and paid recurring plans
- **`purchases`** - One-off plans (e.g., credit top-ups)
<CodeGroup>
```jsx React
import { useCustomer } from "autumn-js/react";
const { data: customer } = useCustomer();
const active = customer?.subscriptions.filter(
(sub) => sub.status === "active"
);
console.log(active?.map((sub) => sub.planId).join(", "));
```
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const active = customer.subscriptions?.filter(
(sub) => sub.status === "active"
);
console.log(active?.map((sub) => sub.planId).join(", "));
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_1234")
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
active = [s for s in customer.subscriptions if s.status == "active"]
print([s.plan_id for s in active])
```
```bash cURL
curl -X POST 'https://api.useautumn.com/v1/customers' \
-H 'Authorization: Bearer am_sk_test_1234' \
-H 'Content-Type: application/json' \
-d '{ "customer_id": "user_123" }'
# Response includes subscriptions array
```
</CodeGroup>
## Usage balances
Metered features have `granted`, `usage`, and `remaining` fields. Use these to display current usage and remaining balance.
<CodeGroup>
```jsx React
import { useCustomer } from "autumn-js/react";
const { data: customer, refetch } = useCustomer();
const messages = customer?.balances.messages;
console.log(`${messages?.remaining} / ${messages?.granted}`);
// After tracking usage or changing plans, call refetch() to update balances
await refetch();
```
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const messages = customer.balances?.messages;
console.log(`${messages?.remaining} / ${messages?.granted}`);
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_1234")
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
messages = customer.balances.get("messages")
print(f"{messages.remaining} / {messages.granted}")
```
```bash cURL
curl -X POST 'https://api.useautumn.com/v1/customers' \
-H 'Authorization: Bearer am_sk_test_1234' \
-H 'Content-Type: application/json' \
-d '{ "customer_id": "user_123" }'
# Response includes balances.[feature_id]
```
</CodeGroup>
## Stripe billing portal
The Stripe billing portal lets users manage their payment method, view past invoices, and cancel their plan.
@@ -461,7 +478,7 @@ curl -X POST 'https://api.useautumn.com/v1/billing.open_customer_portal' \
</CodeGroup>
## Usage history chart
## Usage timeseries chart
Autumn provides aggregate time series queries for usage data. Pass the response to a charting library like Recharts.
@@ -515,3 +532,5 @@ curl -X POST 'https://api.useautumn.com/v1/events.aggregate' \
}'
```
</CodeGroup>
You can also use the [`events.list`](/api-reference/events/listEvents) method to get the raw event data and display it in a table.

View File

@@ -63,14 +63,18 @@ Push changes with `atmn push`.
## Billing methods
| Method | Behavior |
|--------|----------|
| **Prepaid** | Customer commits to a quantity at checkout and pays immediately. To change quantity, they update their subscription. |
| **Usage-based** | Customer is billed for the actual number of units at the end of each billing cycle. |
| Method | When charged | Quantity | Best for |
|--------|-------------|----------|----------|
| **Prepaid** | Upfront at purchase | Customer selects a fixed quantity | Seat licenses with committed counts |
| **Usage-based** | End of billing cycle (prorated on changes) | Automatic — tracks actual usage | Seats that fluctuate frequently |
### Prepaid per-unit
With prepaid, the customer selects a quantity when purchasing. Pass the quantity via `options`:
With prepaid, the customer selects a **total quantity** when purchasing. The `quantity` includes any free included amount — Autumn subtracts the included amount and charges for the remainder.
For example, with 5 included seats at \$10/extra seat, a customer who selects `quantity: 10` gets 10 seats total and pays for 5 extra seats (\$50/month).
Pass the quantity via `featureQuantities`:
<CodeGroup>
@@ -79,11 +83,11 @@ import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_..." });
const { data } = await autumn.checkout({
customer_id: "user_123",
plan_id: "pro",
options: [{
feature_id: "seats",
const { data } = await autumn.billing.attach({
customerId: "user_123",
planId: "pro",
featureQuantities: [{
featureId: "seats",
quantity: 10,
}],
});
@@ -94,10 +98,10 @@ from autumn_sdk import Autumn
autumn = Autumn("am_sk_...")
response = await autumn.checkout(
response = await autumn.billing.attach(
customer_id="user_123",
plan_id="pro",
options=[{
feature_quantities=[{
"feature_id": "seats",
"quantity": 10,
}],
@@ -105,13 +109,13 @@ response = await autumn.checkout(
```
```bash cURL
curl -X POST "https://api.useautumn.com/v1/checkout" \
curl -X POST "https://api.useautumn.com/v1/billing/attach" \
-H "Authorization: Bearer am_sk_..." \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_123",
"plan_id": "pro",
"options": [{
"feature_quantities": [{
"feature_id": "seats",
"quantity": 10
}]
@@ -120,9 +124,15 @@ curl -X POST "https://api.useautumn.com/v1/checkout" \
</CodeGroup>
The customer's balance is set to the total quantity (10). If they're upgrading and already have seats in use, the existing usage is carried over — so a customer with 3 seats in use would see a remaining balance of 7.
<Note>
Autumn does not prevent you from passing a `quantity` lower than the customer's current usage. If the customer has 5 seats in use and you pass `quantity: 3`, the balance goes negative (-2). The `check` endpoint will return `allowed: false`, preventing new seats from being added, but existing seats are not forcibly removed.
</Note>
### Usage-based per-unit
With usage-based billing, track seat additions and removals as they happen. Autumn bills the total at the end of the billing cycle.
With usage-based billing, no quantity is needed at purchase time. Track seat additions and removals as they happen, and Autumn bills for the actual number of seats in use.
<CodeGroup>
@@ -131,11 +141,19 @@ import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_..." });
// Add a seat
await autumn.track({
customer_id: "user_123",
feature_id: "seats",
value: 1,
});
// Remove a seat
await autumn.track({
customer_id: "user_123",
feature_id: "seats",
value: -1,
});
```
```python Python
@@ -143,14 +161,23 @@ from autumn_sdk import Autumn
autumn = Autumn("am_sk_...")
# Add a seat
await autumn.track(
customer_id="user_123",
feature_id="seats",
value=1,
)
# Remove a seat
await autumn.track(
customer_id="user_123",
feature_id="seats",
value=-1,
)
```
```bash cURL
# Add a seat
curl -X POST "https://api.useautumn.com/v1/track" \
-H "Authorization: Bearer am_sk_..." \
-H "Content-Type: application/json" \
@@ -163,6 +190,42 @@ curl -X POST "https://api.useautumn.com/v1/track" \
</CodeGroup>
When a customer purchases the plan, any seats already in use are **automatically reflected** in their subscription from day one. For example, if a customer has 3 seats in use and purchases a plan with 5 included seats at \$10/extra seat:
- Their balance starts at 5 (the included amount)
- The 3 existing seats are carried over, leaving a remaining balance of 2
- No extra charge yet — they're within the included amount
- As they add seats beyond 5, each additional seat is billed at \$10/month with [proration](/documentation/modelling-pricing/proration)
## Existing usage on upgrade
When a customer upgrades from one plan to another, Autumn **automatically carries over** their current seat usage to the new plan. This ensures there's no gap in tracking — existing seats don't disappear or go unbilled.
### Prepaid
The customer's balance is set to their chosen quantity. Existing usage is then deducted from that balance.
> **Example**: Customer has **3 seats** in use. They purchase a plan with 5 included seats, passing `quantity: 10`.
> - Balance is set to 10 (5 included + 5 purchased)
> - 3 existing seats are deducted → **7 remaining**
> - Stripe charges for 10 seats (with 5 in the free tier)
### Usage-based
No quantity is needed. The Stripe subscription quantity is set to the customer's current usage automatically.
> **Example**: Customer has **3 seats** in use. They purchase a plan with 5 included seats at \$10/extra seat.
> - Balance starts at 5 (included amount)
> - 3 existing seats are deducted → **2 remaining**
> - Stripe subscription reflects 3 seats in use (within the free tier, so no extra charge)
> - When they add a 6th seat, billing begins at \$10/seat for the overage
| Scenario | Prepaid (qty: 8) | Usage-based |
|----------|------------------|-------------|
| **3 in use, 5 included** | Balance: 8 → 5 remaining. Charged for 3 extra. | Balance: 5 → 2 remaining. No extra charge. |
| **3 in use, 0 included** | Balance: 8 → 5 remaining. Charged for 8. | Balance: 0 → -3. Charged for 3 seats. |
| **7 in use, 5 included** | Balance: 8 → 1 remaining. Charged for 3 extra. | Balance: 5 → -2. Charged for 2 extra seats. |
## Checking access
Before allowing a user to add a new seat, check if they have capacity:
@@ -176,7 +239,7 @@ const { data } = await autumn.check({
});
if (!data.allowed) {
// Prompt user to purchase more seats
// Prompt user to purchase more seats or upgrade
}
```
@@ -187,7 +250,7 @@ response = await autumn.check(
)
if not response.allowed:
# Prompt user to purchase more seats
# Prompt user to purchase more seats or upgrade
```
```bash cURL
@@ -202,6 +265,10 @@ curl -X POST "https://api.useautumn.com/v1/check" \
</CodeGroup>
For **prepaid**, `allowed` is `true` when the customer has remaining prepaid balance (ie. unused seats).
For **usage-based**, `allowed` is `true` as long as the customer has a usage-based price configured — additional seats are simply billed at the per-unit rate, so there's no hard cap.
## Proration on quantity changes
When a customer increases or decreases their seat count mid-billing-cycle, you can configure how the price adjustment is handled. See [Proration](/documentation/modelling-pricing/proration) for details.

View File

@@ -0,0 +1,286 @@
---
title: Prepaid Pricing
description: Charge customers upfront for a quantity of a feature, and draw from it as usage occurs
---
Prepaid pricing lets customers pay for a fixed quantity of a feature upfront. They select how many units they want at purchase time, pay immediately, and their balance is decremented as they use it.
This is in contrast to [usage-based pricing](/documentation/modelling-pricing/usage-based-pricing), where customers are billed for actual usage at the end of a billing cycle.
> **Example** <br />
> An AI platform has a Pro plan at \$20/month that includes:
> - **API Credits**: 500 included for free, then \$10 per 1,000 credits per month (consumable)
> - **Seats**: 3 included for free, then \$5 per seat per month (non-consumable)
>
> A customer selects 3,000 credits and 10 seats. They pay \$20 base + \$25 for 2,500 extra credits + \$35 for 7 extra seats = \$80/month.
## Setting up
<Tabs>
<Tab title="CLI">
Create your features and add them to a plan with `prepaid` prices:
```ts autumn.config.ts
import { feature, item, plan } from 'atmn';
export const apiCredits = feature({
id: 'api_credits',
name: 'API Credits',
type: 'metered',
consumable: true,
});
export const seats = feature({
id: 'seats',
name: 'Seats',
type: 'metered',
consumable: false,
});
export const pro = plan({
id: 'pro',
name: 'Pro',
price: { amount: 20, interval: 'month' },
items: [
item({
featureId: apiCredits.id,
included: 500,
price: {
amount: 10,
billingUnits: 1000,
billingMethod: 'prepaid',
interval: 'month',
},
}),
item({
featureId: seats.id,
included: 3,
price: {
amount: 5,
billingMethod: 'prepaid',
interval: 'month',
},
}),
],
});
```
Push changes with `atmn push`.
</Tab>
<Tab title="Dashboard">
1. Navigate to **Plans** and create or edit a plan
2. Add your features:
- A `metered`, `consumable` feature for credits (e.g., "API Credits") — set an **included** amount (500), a **price** ($10 per 1,000 per month), and billing method **Prepaid**
- A `metered`, `non-consumable` feature for seats (e.g., "Seats") — set an **included** amount (3), a **price** ($5 per seat per month), and billing method **Prepaid**
3. Save the plan
</Tab>
</Tabs>
## How it works
When a plan has prepaid features, customers select a **quantity** at purchase time. This quantity determines:
- **How many units are granted** as their balance
- **How much they're charged**, based on the price and billing units
The `quantity` is the **total** number of feature units the customer will receive, including any included amount.
Using our example plan:
- A customer selects **3,000 API credits**. 500 are included, so they pay for 2,500 → \$10 × (2,500 / 1,000) = **\$25/month** for credits.
- The same customer selects **10 seats**. 3 are included, so they pay for 7 → \$5 × 7 = **\$35/month** for seats.
<Note>
If you pass a `quantity` equal to or less than the included amount, the customer gets the included amount and pays nothing extra for that feature.
</Note>
## Passing `feature_quantities`
When attaching a plan or updating a subscription that contains prepaid features, use the `feature_quantities` parameter to specify how many units the customer wants.
### Attaching a plan
Pass a `feature_quantities` entry for each prepaid feature on the plan:
<CodeGroup>
```typescript TypeScript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_..." });
const { data } = await autumn.billing.attach({
customerId: "user_123",
planId: "pro",
featureQuantities: [
{ featureId: "api_credits", quantity: 3000 },
{ featureId: "seats", quantity: 10 },
],
});
```
```python Python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_...")
response = await autumn.billing.attach(
customer_id="user_123",
plan_id="pro",
feature_quantities=[
{ "feature_id": "api_credits", "quantity": 3000 },
{ "feature_id": "seats", "quantity": 10 },
],
)
```
```bash cURL
curl -X POST "https://api.useautumn.com/v1/billing/attach" \
-H "Authorization: Bearer am_sk_..." \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_123",
"plan_id": "pro",
"feature_quantities": [
{ "feature_id": "api_credits", "quantity": 3000 },
{ "feature_id": "seats", "quantity": 10 }
]
}'
```
</CodeGroup>
### Updating a subscription
To change prepaid quantities on an existing subscription, use `billing.update`. For example, to add more seats mid-cycle:
<CodeGroup>
```typescript TypeScript
await autumn.billing.update({
customerId: "user_123",
planId: "pro",
featureQuantities: [
{ featureId: "api_credits", quantity: 3000 },
{ featureId: "seats", quantity: 15 },
],
});
```
```python Python
await autumn.billing.update(
customer_id="user_123",
plan_id="pro",
feature_quantities=[
{ "feature_id": "api_credits", "quantity": 3000 },
{ "feature_id": "seats", "quantity": 15 },
],
)
```
```bash cURL
curl -X POST "https://api.useautumn.com/v1/billing/update" \
-H "Authorization: Bearer am_sk_..." \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_123",
"plan_id": "pro",
"feature_quantities": [
{ "feature_id": "api_credits", "quantity": 3000 },
{ "feature_id": "seats", "quantity": 15 }
]
}'
```
</CodeGroup>
See [Updating Subscriptions](/documentation/customers/updating-subscriptions) for more on previewing changes. When quantities change mid-cycle, Autumn can prorate the charge — see [Proration](/documentation/modelling-pricing/proration) for configuration options.
## Understanding prepaid balances
Once a customer is attached to a plan with prepaid features, their balance `breakdown` distinguishes between what was included for free and what was purchased.
| Field | Description |
|-------|-------------|
| `included_grant` | The amount granted by the plan for free — the "included" amount configured on the plan item. |
| `prepaid_grant` | The amount purchased via `feature_quantities` — the quantity minus the included amount. |
| `granted` | Top-level total: `included_grant + prepaid_grant` summed across all breakdown items. |
| `remaining` | How much is left to use. |
| `usage` | How much has been consumed. |
Using the plan from our setup, a customer who attaches with 3,000 credits and 10 seats will have:
```json expandable
{
"api_credits": {
"feature_id": "api_credits",
"granted": 3000,
"remaining": 3000,
"usage": 0,
"unlimited": false,
"overage_allowed": false,
"breakdown": [
{
"id": "cus_ent_abc123",
"plan_id": "pro",
"included_grant": 500,
"prepaid_grant": 2500,
"remaining": 3000,
"usage": 0,
"reset": {
"interval": "month",
"resets_at": 1773851121437
},
"price": {
"amount": 10,
"billing_units": 1000,
"billing_method": "prepaid"
},
"expires_at": null
}
]
},
"seats": {
"feature_id": "seats",
"granted": 10,
"remaining": 10,
"usage": 0,
"unlimited": false,
"overage_allowed": false,
"breakdown": [
{
"id": "cus_ent_def456",
"plan_id": "pro",
"included_grant": 3,
"prepaid_grant": 7,
"remaining": 10,
"usage": 0,
"reset": null,
"price": {
"amount": 5,
"billing_units": 1,
"billing_method": "prepaid"
},
"expires_at": null
}
]
}
}
```
Use the [check](/documentation/customers/check) endpoint before allowing a customer to use a prepaid feature, and [track](/documentation/customers/tracking-usage) usage afterwards to decrement their balance.
## Prepaid vs usage-based
| | Prepaid | Usage-based |
|---|---|---|
| **When charged** | Upfront at purchase | End of billing cycle |
| **Customer selects quantity** | Yes, via `feature_quantities` | No |
| **Balance behavior** | Decremented as usage occurs | Accumulated and billed |
| **Best for** | Credits, top-ups, seat licenses | Metered APIs, storage, bandwidth |

View File

@@ -6,7 +6,7 @@ description: Handle mid-cycle plan changes with prorated billing
Proration adjusts billing when a customer changes their subscription mid-cycle — whether upgrading to a higher plan, downgrading, or changing the quantity of a non-consumable feature like seats. Autumn calculates the prorated amount and either charges or credits the customer.
> **Example** <br />
> A customer on a $20/month plan upgrades to a $50/month plan halfway through the billing cycle. They're charged $15 (the prorated difference for the remaining half of the month).
> A customer on a \$20/month plan upgrades to a \$50/month plan halfway through the billing cycle. They're charged \$15 (the prorated difference for the remaining half of the month).
## Setting up

View File

@@ -1,12 +1,12 @@
---
title: Recurring Plans
description: Set up recurring subscription plans for your customers
description: Grant customers a recurring allowance of consumable features like messages, credits, or API calls
---
Subscriptions are the most common way to charge customers on a recurring basis. A subscription plan has a fixed base price that customers pay at a regular interval (monthly, quarterly, annually), and can include features with usage limits or additional usage-based charges.
Recurring plans let you grant customers a fixed allowance of consumable features -- like messages, credits, or API calls -- that resets each billing period. Customers pay a base price at a regular interval (monthly, quarterly, annually), and receive a fresh grant of their included features at the start of each cycle.
> **Example** <br />
> A project management tool offers a Pro plan at $20/month that includes 10 seats, 50GB storage, and SSO access.
> An AI writing tool offers a Pro plan at $20/month that grants 1,000 messages per month. When the billing period resets, the customer's message balance is reset back to 1,000.
## Setting up
@@ -25,12 +25,6 @@ export const messages = feature({
consumable: true,
});
export const sso = feature({
id: 'sso',
name: 'SSO',
type: 'boolean',
});
export const pro = plan({
id: 'pro',
name: 'Pro',
@@ -41,9 +35,6 @@ export const pro = plan({
included: 1000,
reset: { interval: 'month' },
}),
item({
featureId: sso.id,
}),
],
});
```
@@ -57,7 +48,7 @@ Push changes with `atmn push`.
2. Click **Create Plan**
3. Set a **name** and **ID** for the plan (e.g., "Pro", `pro`)
4. Under **Price**, set the amount and select a billing interval (`month`, `quarter`, `semi_annual`, or `year`)
5. Add features to the plan set grant amounts, reset intervals, and prices as needed. These will be granted to the customer once they purchase the plan.
5. Add consumable features to the plan -- set grant amounts and reset intervals. These will be granted to the customer each billing period once they subscribe.
6. Save your changes
</Tab>
@@ -182,30 +173,6 @@ curl -X POST "https://api.useautumn.com/v1/attach" \
"expiresAt": null
}
]
},
"sso": {
"featureId": "sso",
"granted": 1,
"remaining": 1,
"usage": 0,
"unlimited": false,
"overageAllowed": false,
"maxPurchase": null,
"nextResetAt": null,
"breakdown": [
{
"id": "cus_ent_def456",
"planId": "pro",
"includedGrant": 1,
"prepaidGrant": 0,
"remaining": 1,
"usage": 0,
"unlimited": false,
"reset": null,
"price": null,
"expiresAt": null
}
]
}
}
}
@@ -215,8 +182,8 @@ curl -X POST "https://api.useautumn.com/v1/attach" \
When a subscription is created, Autumn:
1. Creates a Stripe subscription with the plan's prices
2. Provisions [balances](/documentation/concepts/balances) for each feature in the plan
3. Starts the billing cycle based on the plan's interval
2. Grants the customer their included [balances](/documentation/concepts/balances) for each consumable feature
3. Starts the billing cycle -- balances reset automatically at the start of each period
## Billing intervals
@@ -234,6 +201,15 @@ You can create a separate plan for each interval you want to support. For exampl
You can also configure a custom `interval_count` to charge at non-standard intervals (e.g., every 2 months).
### Billing interval vs reset interval
The billing interval (how often the customer is charged) and the reset interval (how often their feature balance replenishes) are configured independently. They don't have to match.
> **Example** <br />
> A plan billed at $200/year could grant 100 messages/month. The customer pays once a year, but their message balance resets to 100 every month.
This is useful when you want to offer an annual discount while still metering usage on a shorter cycle.
## Managing subscriptions
Once a customer has an active subscription, you can manage upgrades, downgrades, and cancellations. See [Managing Subscriptions](/documentation/customers/s) for details on:

View File

@@ -6,7 +6,7 @@ description: Bill customers based on actual usage at the end of each billing per
Pay-per-use (usage-based) pricing charges customers based on how much of a feature they actually consume, billed at the end of each billing period. This is ideal for products where usage varies significantly between customers.
> **Example** <br />
> A notification service charges $1 per 1,000 notifications sent. A customer who sends 5,000 notifications in a month pays $5 at the end of that month.
> A notification service charges \$1 per 1,000 notifications sent. A customer who sends 5,000 notifications in a month pays \$5 at the end of that month.
## Setting up

View File

@@ -1,15 +1,17 @@
---
title: Welcome to Autumn
sidebarTitle: Introduction
description: "The open source, drop-in system of record for AI and SaaS monetization."
description: "Drop-in, open-source control layer for AI and SaaS monetization."
---
## What is Autumn?
Autumn is a pricing and billing layer between your application and Stripe. It acts as your source of truth for customer subscription statuses, usage metering and credit balances.
Autumn is a pricing and billing layer between your application and Stripe. It owns the subscription lifecycle, credit ledgers, and entitlement state that you'd otherwise build and maintain yourself.
Instead of billing logic living in your code and database, your app can query Autumn in real-time to check if a customer is allowed to do something (eg, send an AI message, access SSO, etc).
This saves you months of engineering time, and makes pricing changes a simple configuration change.
```mermaid actions={false}
flowchart TD
subgraph app["Your Application"]
@@ -43,22 +45,20 @@ flowchart TD
## Why use Autumn?
AI-style monetization is harder for developers to build and maintain. For reference, OpenAI wrote a [blog post](https://openai.com/index/beyond-rate-limits/) about their in-house system. Over time, you will end up building:
For reference, here's what a production-grade monetization system looks like in the AI era. OpenAI wrote a [blog post](https://openai.com/index/beyond-rate-limits/) about their in-house system.
| Feature | Requirements |
|-----------------------|--------------------------------------------------------------------------------------------------|
| Subscription logic | Checkouts, prorated upgrades, scheduled downgrades, add-ons, trials. 10+ webhook cases. |
| Credit system | Real-time enforcement, periodic vs one-time grants, rollovers, expiration, concurrency control |
| Credit ledgers | Real-time enforcement, periodic vs one-time grants, rollovers, expiration, concurrency control |
| Controls and observability | Auto top ups, spend caps, per-seat allowances, usage analytics, event logs |
| Versioning and grandfathering | Various price IDs, migration scripts, backwards compatibility |
| Enterprise and custom plans | Custom code, tiered pricing, custom credit grants, pilots, expansion logic |
| Edge cases | Plan switching, monthly/annual changes, failed payments, 3DS, race conditions, refunds |
At some point you or your GTM team will want to change your pricing, and you will need to rebuild everything.
Billing starts with a simple checkout flow, and balloons in complexity as you add more features and scale. And when you want to change your pricing, you need to rebuild everything. Yet, it's a critical part of your product that you cannot afford to get wrong.
Autumn replaces this, and offloads all this logic out of your codebase. After you set it up, everything about your pricing can be managed through our dashboard or config file.
It's faster to setup, more flexible, more reliable and saves teams months of engineering time.
You can choose to build this yourself, or use Autumn to offload all this logic out of your codebase. It's less work, more flexible, and more reliable.
@@ -68,29 +68,18 @@ It's faster to setup, more flexible, more reliable and saves teams months of eng
## How is this different?
Stripe has its own metered billing product, and there are others out there too like Metronome, Orb, Lago etc. All of these products allow you to record and bill for usage after an action is taken, making them good for end of month invoicing.
Most billing tools, including Stripe's built-in metering, are designed for post-hoc invoicing: you send usage events, they generate invoices at end of period. Your app still owns who gets access, when limits apply, how downgrades work (and all the other logic described above).
However, prepaid credits and usage limits are becoming the default standard for AI monetization. This needs to work in real-time.
Autumn flips this. The `check` function is a low-latency API designed to be called _before_ an action is taken, to gate access based on the customer's plan and balance. Because it runs before the action, Autumn becomes your system of record for subscriptions and entitlements — not your database or Stripe.
Put simply, Autumn's key differentiator is the `check` function: a low-latency API called designed to be called _before_ an action is taken, to gate access. This sounds trivial, but is a very different product in 2 ways:
This means credit ledgers, real-time enforcement, spend caps, and edge cases around upgrades, downgrades and failed payments are handled automatically. Changing pricing, migrating plans, or setting up custom enterprise contracts become configuration changes instead of code changes.
**Functionally** <br />
Because check runs before the action, Autumn becomes your system of record for pricing and entitlements.
Architecture-wise, `check` is designed to be called inline — before every AI generation, API request, or feature gate. Response times are under 50ms via multi-region caching, with atomic handling of concurrent requests so balances stay consistent at scale.
With other providers, billing is asynchronous: you send usage, they generate invoices later, and _you own the logic and state in between_ — who gets access, when limits apply, how downgrades work etc.
Autumn owns that layer. Credit ledgers, real-time enforcement and spend caps work out of the box. Edge cases around upgrades, downgrades, and failed payments are handled automatically — users always get access to what they've paid for.
That also makes scaling challenges much simpler. Changing pricing, migrating plans, handling downgrades, setting up custom enterprise contracts with rollovers, or launching team billing with per-seat allowances become configuration changes instead of messy logic rewrites.
**Architecturally** <br />
`check` is designed to be called inline, before every AI generation, API request, or feature gate. We achieve response times of under 50ms via multi-region caching, with atomic handling of concurrent requests so credit balances and usage limits stay consistent even at scale.
Unlike other billing providers, Autumn is built on top of Stripe billing instead of replacing it. Your subscriptions, customers, and payment details live in your own Stripe account, so you're never "locked in".
Autumn is built on top of Stripe rather than replacing it. Your subscriptions, customers, and payment details live in your own Stripe account.
<Check>
While Autumn's core focus is credit-based AI monetization, it can be used for any SaaS pricing model. Many of our users have no usage-based features at all, and just prefer the simplicity and developer experience (eg, no webhooks).
While Autumn's core focus is credit-based AI monetization, it handles any SaaS pricing model. Many of our users have no usage-based features at all, and just prefer the developer experience (eg, no webhooks).
</Check>
@@ -98,8 +87,7 @@ While Autumn's core focus is credit-based AI monetization, it can be used for an
## Core concepts
## Core flow
<Steps>
<Step title="Model your pricing in Autumn">

View File

@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "autumn",
@@ -446,6 +445,7 @@
"ag-grid-community": "^34.0.2",
"ag-grid-react": "^34.0.2",
"ai": "^6.0.5",
"atmn": "workspace:*",
"autumn-js": "workspace:*",
"axios": "^1.8.3",
"better-auth": "catalog:",

View File

@@ -12,6 +12,18 @@
"homepage": "https://docs.useautumn.com/api-reference/cli/getting-started",
"main": "dist/compose/index.js",
"types": "dist/src/compose/index.d.ts",
"exports": {
".": {
"types": "./dist/src/compose/index.d.ts",
"import": "./dist/compose/index.js",
"default": "./dist/compose/index.js"
},
"./skills": {
"types": "./src/prompts/skills/index.ts",
"import": "./src/prompts/skills/index.ts",
"default": "./src/prompts/skills/index.ts"
}
},
"type": "module",
"engines": {
"node": ">=16"

View File

@@ -1,10 +1,12 @@
import fs from "node:fs/promises";
import path from "node:path";
import { useState } from "react";
import { customerPrompt } from "../../prompts/customer.js";
import { paymentsPrompt } from "../../prompts/payments.js";
import { pricingPrompt } from "../../prompts/pricing.js";
import { usagePrompt } from "../../prompts/usage.js";
import {
autumnBillingPageContent,
autumnGatingContent,
autumnModellingPricingPlansContent,
autumnSetupContent,
} from "../../prompts/skills/index.js";
const GUIDES_DIR = "autumn-guides";
@@ -27,33 +29,31 @@ export function useCreateGuides() {
const created: string[] = [];
// Always write customer, payments, usage guides
await fs.writeFile(
path.join(guidesPath, "1_Customer_Creation.md"),
customerPrompt,
path.join(guidesPath, "1_Setup.md"),
autumnSetupContent,
"utf-8",
);
created.push("1_Customer_Creation.md");
created.push("1_Setup.md");
await fs.writeFile(
path.join(guidesPath, "2_Accepting_Payments.md"),
paymentsPrompt,
path.join(guidesPath, "2_Gating.md"),
autumnGatingContent,
"utf-8",
);
created.push("2_Accepting_Payments.md");
created.push("2_Gating.md");
await fs.writeFile(
path.join(guidesPath, "3_Tracking_Usage.md"),
usagePrompt,
path.join(guidesPath, "3_Billing_Page.md"),
autumnBillingPageContent,
"utf-8",
);
created.push("3_Tracking_Usage.md");
created.push("3_Billing_Page.md");
// Only write pricing guide if user doesn't have pricing yet (or saveAll is true)
if (options?.saveAll || !hasPricing) {
await fs.writeFile(
path.join(guidesPath, "0_Designing_Pricing.md"),
pricingPrompt,
autumnModellingPricingPlansContent,
"utf-8",
);
created.unshift("0_Designing_Pricing.md");

View File

@@ -1,14 +1,17 @@
import fs from "node:fs/promises";
import path from "node:path";
import { useState } from "react";
import { skills, type Skill } from "../../prompts/skills/index.js";
import { type Skill, skills } from "../../prompts/skills/index.js";
type CreateSkillsState = "idle" | "creating" | "done" | "error";
export type SkillsLocation = ".claude/skills" | ".agents/skills" | "custom";
export interface UseCreateSkillsResult {
create: (targetDir: string, options?: { saveAll?: boolean; hasPricing?: boolean }) => Promise<void>;
create: (
targetDir: string,
options?: { saveAll?: boolean; hasPricing?: boolean },
) => Promise<void>;
state: CreateSkillsState;
filesCreated: string[];
error: string | null;
@@ -20,10 +23,10 @@ export interface UseCreateSkillsResult {
* Skills are saved as SKILL.md files in subdirectories:
*
* <targetDir>/
* autumn-customer/SKILL.md
* autumn-payments/SKILL.md
* autumn-pricing/SKILL.md
* autumn-usage/SKILL.md
* autumn-setup/SKILL.md
* autumn-gating/SKILL.md
* autumn-billing-page/SKILL.md
* autumn-modelling-pricing-plans/SKILL.md
*/
export function useCreateSkills(): UseCreateSkillsResult {
const [state, setState] = useState<CreateSkillsState>("idle");
@@ -47,7 +50,11 @@ export function useCreateSkills(): UseCreateSkillsResult {
// Filter skills based on options
const skillsToCreate = skills.filter((skill) => {
// Skip pricing skill if user already has pricing (unless saveAll is true)
if (skill.id === "autumn-pricing" && !options?.saveAll && options?.hasPricing) {
if (
skill.id === "autumn-pricing" &&
!options?.saveAll &&
options?.hasPricing
) {
return false;
}
return true;

View File

@@ -1,662 +0,0 @@
export const creditSystemDocs = `
# Monetary credits
> Grant your users a currency-based balance of credits, that various features can draw from
When you have multiple features that cost different amounts, you can use a credit system to deduct usage from a single balance. This can be great to simplify billing and usage tracking, especially when you have lots of features.
## Example case
We have a AI chatbot product with 2 different models, and each model costs a different amount to use.
* Basic message: $1 per 100 messages
* Premium message: $10 per 100 messages
And we have the following plans:
* Free tier: $5 credits per month for free
* Pro tier: $10 credits per month, at $10 per month
Users should also be able to top up their balance with more credits.
## Configure Pricing
<Steps>
<Step>
#### Create Features
Create a \`metered\` \`consumable\` feature for each message type, so that we can track the usage of each:
<Frame>
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=8ad6f333a2556a3a0917bee04e284b81" className="block dark:hidden" data-og-width="1070" width="1070" data-og-height="360" height="360" data-path="assets/guides/monetary-credits/features-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=3a4e016041c82b2f20a692d518100e69 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=3970dc71e6fadbad5bba1172c9411608 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=155f852f86832b4d1d842149575152cf 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=541ab4b35244cbec79bd463e9ef8ed55 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=1ac1565047e6af82cbb6d590219e350f 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-light.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=f246bd47edcdb6be84e2022f3f783391 2500w" />
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=effa751e7f90b92522be0c3f06e50ad6" className="hidden dark:block" data-og-width="1080" width="1080" data-og-height="366" height="366" data-path="assets/guides/monetary-credits/features-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=bd04f7957b5a336db0543882519e11f3 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=ad9ec98189db3b8217685e9e4feb6e9a 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=b5990bbe24c226717b1e3ea756c7c5ee 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=f3c7adbc4a98597df64c504bc9d0cf3d 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=9b75fca0b89ed32f9174398bb3330f88 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/features-dark.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=bca42110fa6db4b7ea986e56f9213827 2500w" />
</Frame>
</Step>
<Step>
#### Create Credit System
Now, we'll create a credit system, where we'll define the cost of each message type. We'll define the cost per message in USD:
| Feature | Cost per message (USD) | Credit cost per message (USD) |
| --------------- | ---------------------- | ----------------------------- |
| Basic message | $1 per 100 messages | 0.01 |
| Premium message | $10 per 100 messages | 0.10 |
<Frame style={{ width: "400px" }}>
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=282e723ee9a16d0d925829e4cf93d40c" className="block dark:hidden" data-og-width="878" width="878" data-og-height="770" height="770" data-path="assets/guides/monetary-credits/credit-system-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=fe7219a2d7be1cb1508708adfabab7b4 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=3f2c75336173fe5c56b31eeb4b56b4cf 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=13696bf61448875a0c9d093d9215cc80 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=3ffe1fd218aa822fceb12b73c6ed99ec 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=9cc1a7297d6413be48fbc1d24aa9dced 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-light.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=b0e663dbe4abb9608de40f45727b08d7 2500w" />
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=24fe90d0b7de4030e5abfbfd3bfcc510" className="hidden dark:block" data-og-width="878" width="878" data-og-height="780" height="780" data-path="assets/guides/monetary-credits/credit-system-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=94646af7250125285af0761361fbf158 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=eb32f3f32c4efa925287dd9e2e2b8ac9 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=85b274aad733fdd80aeda185a90f982a 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=2f00363bab7740ed43226496cd43f605 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=ce02b334331a22194fb96ad4a8c9067e 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/credit-system-dark.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=6a6e6a98433f92f0df7ab9f00e61c4c0 2500w" />
</Frame>
</Step>
<Step>
#### Create Free, Pro and Top-up Plans
Let's create our free and pro plans, and add the credits amounts to each.
<Tip>
Make sure to set the \`auto-enable\` flag on the free plan, so that it is automatically assigned to new customers.
</Tip>
<Frame style={{ width: "400px" }}>
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=c2f3cb88b041b79058337be88fc5321c" className="block dark:hidden" data-og-width="1310" width="1310" data-og-height="574" height="574" data-path="assets/guides/monetary-credits/free-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=a7b39af8e8457fe57a8516ba9f9a956a 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=dfdd8ffe32bae8bbea97e330445182cf 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=4be516dcb4b63d37d08880b4800e76a7 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=1c11d91d7f7b76382db52cc2c6a7b206 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=c336fd4bac06be4e09f53d42151cd0ca 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-light.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=2132739bacef8e5933474b6cc1b49d37 2500w" />
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=a5a8315daf7371774dd25c47688307d6" className="hidden dark:block" data-og-width="1330" width="1330" data-og-height="616" height="616" data-path="assets/guides/monetary-credits/free-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=303051c359de5678544da424894eac5f 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=f11d0c2112c78f09481d29fc8ae9795c 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=2cdc753e2c00790c1f8ed967f8c387c9 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=b594366595ebf489ccd01fee9218f7d6 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=5b97d4e19e7f1f1f9129b2d4bf9321f4 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/free-dark.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=f4c9c8d3c2af7c3a8febecd55cd22bac 2500w" />
</Frame>
<br />
<Frame style={{ width: "400px" }}>
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=4556f7bc7346b5cae19ae8df13d64d49" className="block dark:hidden" data-og-width="1298" width="1298" data-og-height="538" height="538" data-path="assets/guides/monetary-credits/pro-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=35b62628f73a08aa9326efa5db589707 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=7f6c74061094115009103acb82fd86ad 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=73962d325c7ff9ee9c251a10784c2128 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=524d080d5e1b015e3069e81fbbf920e6 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=68a158c399e81fde9389a71504411412 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-light.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=02cd364a23eba479d6fb3ef5bb21a2db 2500w" />
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=1dfc33533ae4004af4b9cb9f9b92edb1" className="hidden dark:block" data-og-width="1288" width="1288" data-og-height="544" height="544" data-path="assets/guides/monetary-credits/pro-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=0f264534d6cd5848f388e9d7a4c2acb7 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=da4ff2b8578b5502857b48146d2a886c 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=be9b638af6c74425903a5c095b491f3b 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=ecb577f12b8c551f59c8682c1954b45c 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=21d4d80ad6d792bb95c7db6648ffa00f 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/pro-dark.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=87a11d00b2904704bee8507cf1f7acbf 2500w" />
</Frame>
Then, we'll create our top-up plan. We'll add a price to our credit feature, where each credit is worth $1. These top up credits will be \`one-off\` \`prepaid\` purchases that never expire.
<Frame style={{ width: "500px" }}>
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=61df798fdf6c03425d76512648d55eb6" className="block dark:hidden" data-og-width="1856" width="1856" data-og-height="1456" height="1456" data-path="assets/guides/monetary-credits/top-up-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=2e68a36a6eec0252f66fb393e70e367c 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=934128f84335bfa1a1a488449146f295 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=d203e7f707db801747ce71675df6efd1 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=531a33c5053ecfd7416fc49bbc729c07 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=3fa4284bbafd0897d81b5a2547801260 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-light.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=72a21aa0de16bd8b61a36f758c8777d7 2500w" />
<img src="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=6d361d44926bd47e12fc2e660e62915c" className="hidden dark:block" data-og-width="1850" width="1850" data-og-height="1454" height="1454" data-path="assets/guides/monetary-credits/top-up-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?w=280&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=f55c0d77beabf79bc439f64bbeb67d52 280w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?w=560&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=0cae9ec1a79907d87feaacd9939ade2b 560w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?w=840&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=e845d0b6ff78495872b85b29dc3e8111 840w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?w=1100&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=c897d83927839a33fe90f72904d798bc 1100w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?w=1650&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=7ec3cee7f4ee63e36c722984188e247f 1650w, https://mintcdn.com/autumn/1MuK4iLWEUU1zowQ/assets/guides/monetary-credits/top-up-dark.png?w=2500&fit=max&auto=format&n=1MuK4iLWEUU1zowQ&q=85&s=fd8f471a77e48da8158ae92f68cb3993 2500w" />
</Frame>
</Step>
</Steps>
## Implementation
<Steps>
<Step>
#### Create an Autumn Customer
When your user signs up, create an Autumn customer. This will automatically assign them the Free plan, and grant them $5 credits per month.
<CodeGroup>
\`\`\`jsx React theme={null}
import { useCustomer } from "autumn-js/react";
const App = () => {
const { customer } = useCustomer();
console.log("Autumn customer:", customer);
return <h1>Welcome, {customer?.name || "user"}!</h1>;
};
\`\`\`
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data, error } = await autumn.customers.create({
id: "user_or_org_id_from_auth",
name: "John Yeo",
email: "john@example.com",
});
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn('am_sk_42424242')
async def main():
customer = await autumn.customers.create(
id="user_or_org_id_from_auth",
name="John Yeo",
email="john@example.com",
)
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl --request POST \
--url https://api.useautumn.com/customers \
--header 'Authorization: Bearer am_sk_42424242' \
--header 'Content-Type: application/json' \
--data '{
"id": "user_or_org_id_from_auth",
"name": "John Yeo",
"email": "john@example.com"
}'
\`\`\`
</CodeGroup>
</Step>
<Step>
#### Checking for access
Every time our user sends a message to the chatbot, we'll first check if they have enough credits remaining to send the message.
The \`required_balance\` parameter will convert the number of messages to credits. Eg, if you pass \`required_balance: 5\` for basic messages, then check will return \`allowed: true\` if the user has at least 0.05 USD credits remaining.
<Note>
Note how we're interacting with the underlying features (\`basic_messages\`,
\`premium_messages\`) here--not the credit system.
</Note>
<CodeGroup>
\`\`\`jsx React wrap theme={null}
import { useCustomer } from "autumn-js/react";
export function CheckBasicMessage() {
const { check, refetch } = useCustomer();
const handleCheckAccess = async () => {
const { data } = await check({ featureId: "basic_messages", requiredBalance: 1 });
if (!data?.allowed) {
alert("You've run out of basic message credits");
} else {
// proceed with sending message
await refetch();
}
};
}
\`\`\`
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data } = await autumn.check({
customer_id: "user_or_org_id_from_auth",
feature_id: "basic_messages",
required_balance: 1,
});
if (!data.allowed) {
console.log("User has run out of basic message credits");
return;
}
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_42424242")
async def main():
response = await autumn.check(
customer_id="user_or_org_id_from_auth",
feature_id="basic_messages",
required_balance=1,
)
if not response.allowed:
print("User has run out of basic message credits")
return
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/check" \
-H "Authorization: Bearer am_sk_42424242" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "basic_messages",
"required_balance": 1
}'
\`\`\`
</CodeGroup>
<Expandable title="check response">
The credit system ID will be returned in the \`balances\` field.
\`\`\`json {8} theme={null}
{
"allowed": true,
"code": "feature_found",
"customer_id": "ayush",
"feature_id": "usd_credits",
"required_balance": 0.01,
"interval": "month",
"interval_count": 1,
"unlimited": false,
"balance": 5,
"usage": 0,
"included_usage": 5,
"next_reset_at": 1769110978704,
"overage_allowed": false,
"credit_schema": [
{
"feature_id": "basic_messages",
"credit_amount": 0.01
},
{
"feature_id": "premium_messages",
"credit_amount": 0.1
}
]
}
\`\`\`
</Expandable>
</Step>
<Step>
#### Tracking messages and using credits
Now let's implement our usage tracking and use up our credits. In this example, we're using 2 basic messages, which will cost us 0.02 USD credits.
<CodeGroup>
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
await autumn.track({
customer_id: "user_or_org_id_from_auth",
feature_id: "basic_messages",
value: 2,
});
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_42424242")
async def main():
await autumn.track(
customer_id="user_or_org_id_from_auth",
feature_id="basic_messages",
value=2,
)
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/track" \
-H "Authorization: Bearer am_sk_42424242" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "basic_messages",
"value": 2
}'
\`\`\`
</CodeGroup>
<Expandable title="track response">
\`\`\`json theme={null}
{
"code": "event_received",
"customer_id": "user_or_org_id_from_auth",
"feature_id": "basic_messages"
}
\`\`\`
</Expandable>
</Step>
<Step>
#### Upgrading to Pro
We can prompt the user to upgrade. When they click our "upgrade" button, we can use the \`checkout\` route to get a Stripe Checkout URL for them to make a payment.
<CodeGroup>
\`\`\`jsx React theme={null}
import { useCustomer, CheckoutDialog } from "autumn-js/react";
export default function UpgradeButton() {
const { checkout } = useCustomer();
return (
<button
onClick={async () => {
await checkout({
productId: "pro",
dialog: CheckoutDialog,
});
}}
>
Upgrade to Pro
</button>
);
}
\`\`\`
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data } = await autumn.checkout({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",
});
if (data.url) {
// Redirect user to Stripe checkout URL
} else {
// Show upgrade preview to user
}
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_42424242")
async def main():
response = await autumn.checkout(
customer_id="user_or_org_id_from_auth",
product_id="pro"
)
if response.url:
# Redirect user to Stripe checkout URL
pass
else:
# Show upgrade preview to user
pass
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/checkout" \
-H "Authorization: Bearer am_sk_42424242" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"product_id": "pro"
}'
\`\`\`
</CodeGroup>
<Expandable title="checkout response">
\`\`\`json theme={null}
{
"customer_id": "user_or_org_id_from_auth",
"lines": [
{
"description": "Pro - $10 / month",
"amount": 10,
"item": {
"type": "price",
"feature_id": null,
"feature": null,
"interval": "month",
"interval_count": 1,
"price": 10,
"display": {
"primary_text": "$10",
"secondary_text": "per month"
}
}
}
],
"product": {
"id": "pro",
"name": "Pro",
"group": null,
"env": "sandbox",
"is_add_on": false,
"is_default": false,
"archived": false,
"version": 1,
"created_at": 1766428038264,
"items": [
{
"type": "price",
"feature_id": null,
"feature": null,
"interval": "month",
"interval_count": 1,
"price": 10,
"display": {
"primary_text": "$10",
"secondary_text": "per month"
}
},
{
"type": "feature",
"feature_id": "usd_credits",
"feature_type": "single_use",
"feature": {
"id": "usd_credits",
"name": "USD credits",
"type": "credit_system",
"display": {
"singular": "USD credits",
"plural": "USD credits"
},
"credit_schema": [
{
"metered_feature_id": "basic_messages",
"credit_cost": 0.01
},
{
"metered_feature_id": "premium_messages",
"credit_cost": 0.1
}
]
},
"included_usage": 10,
"interval": "month",
"interval_count": 1,
"reset_usage_when_enabled": true,
"entity_feature_id": null,
"display": {
"primary_text": "10 USD credits"
}
}
],
"free_trial": null,
"base_variant_id": null,
"scenario": "upgrade",
"properties": {
"is_free": false,
"is_one_off": false,
"interval_group": "month",
"has_trial": false,
"updateable": false
}
},
"current_product": {
"id": "free",
"name": "Free",
"group": null,
"env": "sandbox",
"is_add_on": false,
"is_default": true,
"archived": false,
"version": 1,
"created_at": 1766427877578,
"items": [
{
"type": "feature",
"feature_id": "usd_credits",
"feature_type": "single_use",
"feature": {
"id": "usd_credits",
"name": "USD credits",
"type": "credit_system",
"display": {
"singular": "USD credits",
"plural": "USD credits"
},
"credit_schema": [
{
"metered_feature_id": "basic_messages",
"credit_cost": 0.01
},
{
"metered_feature_id": "premium_messages",
"credit_cost": 0.1
}
]
},
"included_usage": 5,
"interval": "month",
"interval_count": 1,
"reset_usage_when_enabled": true,
"entity_feature_id": null,
"display": {
"primary_text": "5 USD credits",
"secondary_text": "per month"
}
}
],
"free_trial": null,
"base_variant_id": null,
"scenario": "new",
"properties": {
"is_free": true,
"is_one_off": false,
"has_trial": false,
"updateable": false
}
},
"options": [],
"total": 10,
"currency": "usd",
"url": "https://checkout.stripe.com/c/pay/.......",
"has_prorations": false
}
\`\`\`
</Expandable>
</Step>
<Step>
#### Purchasing Top-ups
When users run low on credits, they can purchase additional credits using our top-up plan. In this example, the user is purchasing 20 USD credits, which will cost them $20.
<CodeGroup>
\`\`\`jsx React theme={null}
import { useCustomer, CheckoutDialog } from "autumn-js/react";
export default function TopUpButton() {
const { checkout } = useCustomer();
return (
<button
onClick={async () => {
await checkout({
productId: "top_up",
dialog: CheckoutDialog,
options: [{
featureId: "usd_credits",
quantity: 20,
}],
});
}}
>
Buy More Credits
</button>
);
}
\`\`\`
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data } = await autumn.attach({
customer_id: "user_or_org_id_from_auth",
product_id: "top_up",
options: [{
feature_id: "usd_credits",
quantity: 20,
}],
});
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_42424242")
async def main():
response = await autumn.attach(
customer_id="user_or_org_id_from_auth",
product_id="top_up",
options=[{
"feature_id": "usd_credits",
"quantity": 20,
}],
)
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/attach" \
-H "Authorization: Bearer am_sk_42424242" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"product_id": "top_up",
"options": [{
"feature_id": "usd_credits",
"quantity": 20,
}]
}'
\`\`\`
</CodeGroup>
<Expandable title="attach response">
\`\`\`json theme={null}
{
"success": true,
"customer_id": "user_or_org_id_from_auth",
"product_ids": [
"top_up"
],
"code": "one_off_product_attached",
"message": "Successfully purchased product(s) Top up and attached to customer John"
}
\`\`\`
</Expandable>
</Step>
</Steps>
---
> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.useautumn.com/llms.txt
`;

View File

@@ -1,296 +0,0 @@
export const customerPrompt = `## Set up Autumn billing integration
Autumn is a billing and entitlements layer over Stripe, which we are adding into this codebase. Your task is to create an Autumn customer by following these steps, and add it to a place in this app where it will be automatically created.
### Step 1: Analyze my codebase
Before making changes, detect:
- Language (TypeScript/JavaScript, Python, or other)
- If TS/JS: Framework (Next.js, React Router, Tanstack Start, Hono, Express, Fastify, or other)
- If TS/JS: Is there a React frontend? (Check for React in package.json)
Also ask me:
**1. Should Autumn customers be individual users, or organizations?**
- Users (B2C): Each user has their own plan and limits
- Organizations (B2B): Plans and limits are shared across an org
**2. Have you created an AUTUMN_SECRET_KEY and added it to .env?**
Please prompt them to create one here: https://app.useautumn.com/dev?tab=api_keys and add it to .env as AUTUMN_SECRET_KEY
Tell me what you detected, which path you'll follow and what you'll be adding autumn to.
---
## Path A: React + Node.js (fullstack TypeScript)
Use this path if there's a React frontend with a Node.js backend.
### A1. Install the SDK
**Use the package manager already installed** -- eg user may be using bun, or pnpm.
\`\`\`bash
npm install autumn-js
\`\`\`
### A2. Mount the handler (server-side)
This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn.
**Next.js (App Router):**
\`\`\`typescript
// app/api/autumn/[...all]/route.ts
import { autumnHandler } from "autumn-js/next";
export const { GET, POST } = autumnHandler({
identify: async (request) => {
// Get user/org from your auth provider
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: {
name: session?.user.name,
email: session?.user.email,
},
};
},
});
\`\`\`
**React Router:**
\`\`\`typescript
// app/routes/api.autumn.tsx
import { autumnHandler } from "autumn-js/react-router";
export const { loader, action } = autumnHandler({
identify: async (args) => {
const session = await auth.api.getSession({ headers: args.request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
});
// routes.ts - add this route
route("api/autumn/*", "routes/api.autumn.tsx")
\`\`\`
**Tanstack Start:**
\`\`\`typescript
// routes/api/autumn.$.ts
import { autumnHandler } from "autumn-js/tanstack";
const handler = autumnHandler({
identify: async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
});
export const Route = createFileRoute("/api/autumn/$")({
server: { handlers: handler },
});
\`\`\`
**Hono:**
\`\`\`typescript
import { autumnHandler } from "autumn-js/hono";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
\`\`\`
**Express:**
\`\`\`typescript
import { autumnHandler } from "autumn-js/express";
app.use(express.json()); // Must be before autumnHandler
app.use("/api/autumn", autumnHandler({
identify: async (req) => {
const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
\`\`\`
**Fastify:**
\`\`\`typescript
import { autumnHandler } from "autumn-js/fastify";
fastify.route({
method: ["GET", "POST"],
url: "/api/autumn/*",
handler: autumnHandler({
identify: async (request) => {
const session = await auth.api.getSession({ headers: request.headers as any });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}),
});
\`\`\`
**Other frameworks (generic handler):**
\`\`\`typescript
import { autumnHandler } from "autumn-js/backend";
// Mount this handler onto the /api/autumn/* path in your backend
const handleRequest = async (request) => {
// Your authentication logic here
const customerId = "user_or_org_id_from_auth";
let body = null;
if (request.method !== "GET") {
body = await request.json();
}
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: request.url,
method: request.method,
body: body,
},
});
return new Response(JSON.stringify(response), {
status: statusCode,
headers: { "Content-Type": "application/json" },
});
};
\`\`\`
### A3. Add the provider (client-side)
Wrap your app with AutumnProvider:
\`\`\`tsx
import { AutumnProvider } from "autumn-js/react";
export default function RootLayout({ children }) {
return (
<AutumnProvider>
{children}
</AutumnProvider>
);
}
\`\`\`
If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`:
\`\`\`tsx
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
\`\`\`
### A4. Create a test customer
Add this hook to any component to verify the integration:
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { customer } = useCustomer();
console.log("Autumn customer:", customer);
\`\`\`
This automatically creates an Autumn customer for new users/orgs.
---
## Path B: Backend only (Node.js, Python, or other)
Use this path if there's no React frontend, or you prefer server-side only.
### B1. Install the SDK
\`\`\`bash
# Node.js
npm install autumn-js
# Python
pip install autumn-py
\`\`\`
### B2. Initialize the client
**TypeScript/JavaScript:**
\`\`\`typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
\`\`\`
**Python:**
\`\`\`python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
\`\`\`
### B3. Create a test customer
This will GET or CREATE a new customer. Add it when a user signs in or loads the app. Pass in ID from auth provider.
The response returns customer state, used to display billing information client-side. Please console.log the Autumn customer client-side.
**TypeScript:**
\`\`\`typescript
const { data, error } = await autumn.customers.create({
id: "user_or_org_id_from_auth",
name: "Test User",
email: "test@example.com",
});
\`\`\`
**Python:**
\`\`\`python
customer = await autumn.customers.create(
id="user_or_org_id_from_auth",
name="Test User",
email="test@example.com",
)
\`\`\`
**cURL:**
\`\`\`bash
curl -X POST https://api.useautumn.com/customers \\
-H "Authorization: Bearer am_sk_test_xxx" \\
-H "Content-Type: application/json" \\
-d '{"id": "user_or_org_id_from_auth", "name": "Test User", "email": "test@example.com"}'
\`\`\`
When calling these functions from the client, the SDK exports types for all response objects. Use these for type-safe code.
\`\`\`tsx
import type { Customer } from "autumn-js";
\`\`\`
---
## Verify
After setup, tell me:
1. What stack you detected
2. Which path you followed
3. What files you created/modified
4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard
**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root.
Docs: https://docs.useautumn.com/llms.txt`;

View File

@@ -1,254 +0,0 @@
import { prepaidDocs } from "./prepaidDocs.js";
export const paymentsPrompt = `## Add Autumn payment flow
Autumn handles Stripe checkout and plan changes. Your task is to add the payment flow to this codebase for ALL plans in the Autumn configuration.
### Step 1: Detect my integration type
Check if this codebase already has Autumn set up:
- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted → **Path A: React**
- If there's just an \`Autumn\` client initialized → **Path B: Backend SDK**
Before implementing:
1. Tell me which path you'll follow before proceeding.
2. Tell me that I will be building pricing cards to handle billing flows, and ask for any guidance or any input
---
## Path A: React
### Checkout Flow
Use \`checkout\` from \`useCustomer\`. It returns either a Stripe URL (new customer) or checkout preview data (returning customer with card on file).
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { checkout } = useCustomer();
const data = await checkout({ productId: "pro" });
if (!data.url) {
// Returning customer → show confirmation dialog with result data
// data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle }
}
\`\`\`
After user confirms in your dialog, call \`attach\` to enable plan (and charge card as needed)
\`\`\`tsx
const { attach } = useCustomer();
await attach({ productId: "pro" });
\`\`\`
### Getting Billing State
Use \`usePricingTable\` to get products with their billing scenario and display state.
\`\`\`tsx
import { usePricingTable } from "autumn-js/react";
function PricingPage() {
const { products } = usePricingTable();
// Each product has: scenario, properties
// scenario: "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel"
}
\`\`\`
### Canceling
Only use this if there is no free plan in the user's Autumn config. If there is a free plan, then you can cancel by attaching the free plan.
\`\`\`tsx
const { cancel } = useCustomer();
await cancel({ productId: "pro" });
\`\`\`
---
## Path B: Backend SDK
### Checkout Flow
Payments are a 2-step process:
1. **checkout** - Returns Stripe checkout URL (new customer) or preview data (returning customer)
2. **attach** - Confirms purchase when no URL was returned
**TypeScript:**
\`\`\`typescript
import { Autumn } from "autumn-js";
import type { CheckoutResult, AttachResult } from "autumn-js";
const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY });
// Step 1: Get checkout info
const { data } = await autumn.checkout({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",
}) as { data: CheckoutResult };
if (data.url) {
// New customer → redirect to Stripe
return redirect(data.url);
} else {
// Returning customer → return preview data for confirmation UI
// data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle }
return data;
}
// Step 2: After user confirms (only if no URL)
const { data: attachData } = await autumn.attach({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",
}) as { data: AttachResult };
\`\`\`
**Python:**
\`\`\`python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
# Step 1: Get checkout info
response = await autumn.checkout(
customer_id="user_or_org_id_from_auth",
product_id="pro",
)
if response.url:
# New customer → redirect to Stripe
return redirect(response.url)
else:
# Returning customer → return preview data for confirmation UI
return response
# Step 2: After user confirms
attach_response = await autumn.attach(
customer_id="user_or_org_id_from_auth",
product_id="pro",
)
\`\`\`
For prepaid pricing options, see the end of this message.
### Getting Billing State
Use \`products.list\` with a \`customer_id\` to get products with their billing scenario. **Don't build custom billing state logic.**
**TypeScript:**
\`\`\`typescript
const { data } = await autumn.products.list({
customer_id: "user_or_org_id_from_auth",
});
data.list.forEach((product) => {
const { scenario } = product;
// "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel"
});
\`\`\`
**Python:**
\`\`\`python
response = await autumn.products.list(customer_id="user_or_org_id_from_auth")
for product in response.list:
scenario = product.scenario
\`\`\`
**curl:**
\`\`\`bash
curl https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth \\
-H "Authorization: Bearer $AUTUMN_SECRET_KEY"
\`\`\`
### Canceling
\`\`\`typescript
await autumn.cancel({ customer_id: "...", product_id: "pro" });
\`\`\`
Or attach a free product ID to downgrade.
---
## Common Patterns
### Pricing Button Text
\`\`\`typescript
const SCENARIO_TEXT: Record<string, string> = {
scheduled: "Plan Scheduled",
active: "Current Plan",
renew: "Renew",
upgrade: "Upgrade",
new: "Enable",
downgrade: "Downgrade",
cancel: "Cancel Plan",
};
export const getPricingButtonText = (product: Product): string => {
const { scenario, properties } = product;
const { is_one_off, updateable, has_trial } = properties ?? {};
if (has_trial) return "Start Trial";
if (scenario === "active" && updateable) return "Update";
if (scenario === "new" && is_one_off) return "Purchase";
return SCENARIO_TEXT[scenario ?? ""] ?? "Enable Plan";
};
\`\`\`
### Confirmation Dialog Text
\`\`\`typescript
import type { CheckoutResult, Product } from "autumn-js";
export const getConfirmationTexts = (result: CheckoutResult): { title: string; message: string } => {
const { product, current_product, next_cycle } = result;
const scenario = product.scenario;
const productName = product.name;
const currentProductName = current_product?.name;
const nextCycleDate = next_cycle?.starts_at
? new Date(next_cycle.starts_at).toLocaleDateString()
: undefined;
const isRecurring = !product.properties?.is_one_off;
const CONFIRMATION_TEXT: Record<string, { title: string; message: string }> = {
scheduled: { title: "Already Scheduled", message: "You already have this product scheduled." },
active: { title: "Already Active", message: "You are already subscribed to this product." },
renew: { title: "Renew", message: \`Renew your subscription to \${productName}.\` },
upgrade: { title: \`Upgrade to \${productName}\`, message: \`Upgrade to \${productName}. Your card will be charged immediately.\` },
downgrade: { title: \`Downgrade to \${productName}\`, message: \`\${currentProductName} will be cancelled. \${productName} begins \${nextCycleDate}.\` },
cancel: { title: "Cancel", message: \`Your subscription to \${currentProductName} will end \${nextCycleDate}.\` },
};
if (scenario === "new") {
return isRecurring
? { title: \`Subscribe to \${productName}\`, message: \`Subscribe to \${productName}. Charged immediately.\` }
: { title: \`Purchase \${productName}\`, message: \`Purchase \${productName}. Charged immediately.\` };
}
return CONFIRMATION_TEXT[scenario ?? ""] ?? { title: "Change Subscription", message: "You are about to change your subscription." };
};
\`\`\`
---
## Notes
- **NB: the result is \`data.url\`, NOT \`data.checkout_url\`**
- This handles all upgrades, downgrades, renewals, uncancellations automatically
- Product IDs come from the Autumn configuration
- Pass \`successUrl\` to \`checkout\` to redirect users after payment
- For prepaid pricing examples, see the end of this message.
**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root.
Docs: https://docs.useautumn.com/llms.txt
<Prepaid Documentation>
${prepaidDocs}
</Prepaid Documentation>
`;

View File

@@ -1,463 +0,0 @@
export const prepaidDocs = `
# Prepaid top-ups
> Let customers purchase prepaid packages and top-ups.
If a user hits a usage limit you granted them, they may be willing to purchase a top-up.
These are typically one-time purchases (or less commonly, recurring add-ons) that grant a fixed usage of a feature.
This gives users full spend control and allows your business to be paid upfront. For these reasons, it tends to be a more popular alternative to usage-based pricing -- eg, OpenAI uses this model for their API.
## Example case
In this example, we have an AI chatbot that offers:
* 10 premium messages for free
* An option for customers to top-up premium messages in packages of $10 per 100 messages.
## Configure Pricing
<Steps>
<Step>
#### Create Features
Create a \`metered\` \`consumable\` feature for our premium messages, so we can track its balance.
<Frame>
<img src="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=7d51661d2e27ba1f0c8dcbafe7e26cf5" className="block dark:hidden" data-og-width="1128" width="1128" data-og-height="292" height="292" data-path="assets/guides/prepaid/features-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?w=280&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=d54dfa034d29e0e4a1223bc70bf161b0 280w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?w=560&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=c9ba4ccf01f80db253eda16a13b67ffd 560w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?w=840&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=2831cd73822a2917c0c623901d0db448 840w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?w=1100&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=17bff4662f924660ff190afa9a4ac714 1100w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?w=1650&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=78b20f0dde3d8de292eef45c4f71798c 1650w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-light.png?w=2500&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=a3562a182e22d62a3ee3a7499e6f9ae5 2500w" />
<img src="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=46e63f36c563c947c323d59d88d4dee5" className="hidden dark:block" data-og-width="1138" width="1138" data-og-height="298" height="298" data-path="assets/guides/prepaid/features-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?w=280&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=529040483e4f8c4a0a74605d2f787558 280w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?w=560&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=2a9a7b6c0f017ccfa08c4fe05dac7c97 560w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?w=840&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=f2a31b84629229a3fb42d750352e0312 840w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?w=1100&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=3304704796e7376f0ebb303d0819265e 1100w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?w=1650&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=66ebaae97cf4f81d005fd10bb32398fc 1650w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/features-dark.png?w=2500&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=139ebfbf2e7b3f3067d3e1c893eecc9f 2500w" />
</Frame>
</Step>
<Step>
#### Create Free and Top-up Plans
Create our free plan, and assign 10 premium messages to it. These are "one-off" credits, that will not reset periodically.
<Tip>
Make sure to set the \`auto-enable\` flag on the free plan, so that it is automatically assigned to new customers.
</Tip>
<Frame>
<img src="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=0a0c4dc3070a689e0a37cf9db6bbe003" className="block dark:hidden" data-og-width="1262" width="1262" data-og-height="518" height="518" data-path="assets/guides/prepaid/free-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?w=280&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=ec21ed1330917d23eb1f750127e23452 280w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?w=560&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=8d8047fa514b6aa1d853cef259ca14cc 560w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?w=840&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=72a9d26668712eaef8bf16e7f948d228 840w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?w=1100&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=4797ce0fd5cfe702f019f39f00d15f7d 1100w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?w=1650&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=0521e3d328a84b760e1535b7a5bd9f20 1650w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-light.png?w=2500&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=bd57d59f066d9075ddcd115a9539991e 2500w" />
<img src="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=e7c4a18129533d976c97f3e6f899dd5f" className="hidden dark:block" data-og-width="1302" width="1302" data-og-height="534" height="534" data-path="assets/guides/prepaid/free-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?w=280&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=fe5774aa4ed885b2f0259831c5d848d3 280w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?w=560&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=cc8630ef5f921c527b2798cfd042710a 560w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?w=840&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=651d5be440c865e4dedc227660fd623f 840w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?w=1100&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=af7ab38d1bd0fca672fe2cb7d38db32a 1100w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?w=1650&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=97154c5b887d1152667717d0ccab3a60 1650w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/free-dark.png?w=2500&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=90b96fe496eafb60a0e590dd5b547810 2500w" />
</Frame>
Now we'll create our top-up plan. We'll add a price to our premium messages feature, at $10 per 100 messages. These are "one-off" purchases, with a \`prepaid\` billing method.
\`prepaid\` features require a \`quantity\` to be sent in when a customer attaches this product, so the customer can specify how many premium messages they want to top up with.
<Frame>
<img src="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=080df829cf28a99c78a42c214c9aa6b0" className="block dark:hidden" data-og-width="1828" width="1828" data-og-height="1488" height="1488" data-path="assets/guides/prepaid/topup-light.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?w=280&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=261f79b4a45ce98eab3d7e5bd310d277 280w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?w=560&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=c740e2c6c421e17ff1dfde6e12d9cdf6 560w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?w=840&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=9810f426e38655448b47e1b66ae26124 840w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?w=1100&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=5bafa09596f46fea9984a0de6393b5d4 1100w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?w=1650&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=dcf647b6b8d4ba24ac17c180d8cf3f3f 1650w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-light.png?w=2500&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=3e52534b34a58fc8a12bf9892dbaa7b1 2500w" />
<img src="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=083d64784c2abb76cfd147a829eeb1c5" className="hidden dark:block" data-og-width="1824" width="1824" data-og-height="1498" height="1498" data-path="assets/guides/prepaid/topup-dark.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?w=280&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=ec40cfd05340fa7537bf954168db3547 280w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?w=560&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=0bbd4b8c2816c16e9bd118a2bfb523d8 560w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?w=840&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=637c076a3c696957f2eee3c46f0a5bbe 840w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?w=1100&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=ba7b4d72576832cc91c09712c7bdf99b 1100w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?w=1650&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=7b7334526aed1489605b6c95f0942697 1650w, https://mintcdn.com/autumn/s33U5Ef8txzRP-3W/assets/guides/prepaid/topup-dark.png?w=2500&fit=max&auto=format&n=s33U5Ef8txzRP-3W&q=85&s=f24b74fefc853031a1fa981c9b95bd28 2500w" />
</Frame>
</Step>
</Steps>
## Implementation
<Steps>
<Step>
#### Create an Autumn Customer
When your user signs up, create an Autumn customer. This will automatically assign them the Free plan, and grant them 10 premium messages.
<CodeGroup>
\`\`\`jsx React
import { useCustomer } from "autumn-js/react";
const App = () => {
const { customer } = useCustomer();
console.log("Autumn customer:", customer);
return <h1>Welcome, {customer?.name || "user"}!</h1>;
};
\`\`\`
\`\`\`typescript Node.js
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data, error } = await autumn.customers.create({
id: "user_or_org_id_from_auth",
name: "John Yeo",
email: "john@example.com",
});
\`\`\`
\`\`\`python Python
import asyncio
from autumn import Autumn
autumn = Autumn('am_sk_42424242')
async def main():
customer = await autumn.customers.create(
id="user_or_org_id_from_auth",
name="John Yeo",
email="john@example.com",
)
asyncio.run(main())
\`\`\`
\`\`\`bash cURL
curl --request POST \
--url https://api.useautumn.com/customers \
--header 'Authorization: Bearer am_sk_42424242' \
--header 'Content-Type: application/json' \
--data '{
"id": "user_or_org_id_from_auth",
"name": "John Yeo",
"email": "john@example.com"
}'
\`\`\`
</CodeGroup>
</Step>
<Step>
#### Checking for access
Every time our user wants to send a premium message, we'll first check if they have enough premium messages remaining.
<CodeGroup>
\`\`\`jsx React wrap
import { useCustomer } from "autumn-js/react";
export function CheckPremiumMessage() {
const { check, refetch } = useCustomer();
const handleCheckAccess = async () => {
const { data } = await check({ featureId: "premium-messages" });
if (!data?.allowed) {
alert("You've run out of premium messages");
} else {
// proceed with sending message
await refetch();
}
};
}
\`\`\`
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data } = await autumn.check({
customer_id: "user_or_org_id_from_auth",
feature_id: "premium_messages",
});
if (!data.allowed) {
console.log("User has run out of premium messages");
return;
}
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_1234567890")
async def main():
response = await autumn.check(
customer_id="user_or_org_id_from_auth",
feature_id="premium_messages",
)
if not response.allowed:
print("User has run out of premium messages")
return
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/check" \
-H "Authorization: Bearer am_sk_1234567890" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "premium_messages"
}'
\`\`\`
</CodeGroup>
<Expandable title="check response">
\`\`\`json theme={null}
{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "premium_messages",
"code": "feature_found",
"allowed": true,
"balance": 10,
"usage": 0,
"included_usage": 10,
"unlimited": false,
"interval": null,
"interval_count": 1,
"next_reset_at": null,
"overage_allowed": false
}
\`\`\`
</Expandable>
</Step>
<Step>
#### Tracking premium messages
Now let's implement our usage tracking and use up our premium messages. In this example, we're using 5 premium messages.
<CodeGroup>
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
await autumn.track({
customer_id: "user_or_org_id_from_auth",
feature_id: "premium_messages",
value: 5,
});
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_42424242")
async def main():
await autumn.track(
customer_id="user_or_org_id_from_auth",
feature_id="premium_messages",
value=5,
)
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/track" \
-H "Authorization: Bearer am_sk_42424242" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "premium_messages",
"value": 5
}'
\`\`\`
</CodeGroup>
<Expandable title="track response">
\`\`\`json theme={null}
{
"code": "event_received",
"customer_id": "user_or_org_id_from_auth",
"feature_id": "premium_messages"
}
\`\`\`
</Expandable>
</Step>
<Step>
#### Purchasing top-ups
When users run out of premium messages, they can purchase additional messages using our top-up plan. In this example, the user is purchasing 200 premium messages, which will cost them $20.
<CodeGroup>
\`\`\`jsx React theme={null}
import { useCustomer, CheckoutDialog } from "autumn-js/react";
export default function TopUpButton() {
const { checkout } = useCustomer();
return (
<button
onClick={async () => {
await checkout({
productId: "top_up",
dialog: CheckoutDialog,
options: [{
featureId: "premium_messages",
quantity: 200,
}],
});
}}
>
Buy More Messages
</button>
);
}
\`\`\`
\`\`\`typescript Node.js theme={null}
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: 'am_sk_42424242',
});
const { data } = await autumn.checkout({
customer_id: "user_or_org_id_from_auth",
product_id: "top_up",
options: [{
feature_id: "premium_messages",
quantity: 200,
}],
});
if (data.url) {
// Redirect user to Stripe checkout URL
} else {
// Show purchase preview to user
}
\`\`\`
\`\`\`python Python theme={null}
import asyncio
from autumn import Autumn
autumn = Autumn("am_sk_42424242")
async def main():
response = await autumn.checkout(
customer_id="user_or_org_id_from_auth",
product_id="top-up",
options=[{
"feature_id": "premium-messages",
"quantity": 200,
}],
)
if response.url:
# Redirect user to Stripe checkout URL
pass
else:
# Show purchase preview to user
pass
asyncio.run(main())
\`\`\`
\`\`\`bash cURL theme={null}
curl -X POST "https://api.useautumn.com/v1/checkout" \
-H "Authorization: Bearer am_sk_42424242" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "user_or_org_id_from_auth",
"product_id": "top-up",
"options": [{
"feature_id": "premium-messages",
"quantity": 200
}]
}'
\`\`\`
</CodeGroup>
<Expandable title="checkout response">
\`\`\`json theme={null}
{
"customer_id": "user_or_org_id_from_auth",
"lines": [
{
"description": "Top-up - 200 premium messages",
"amount": 20,
"item": {
"type": "feature",
"feature_id": "premium-messages",
"feature_type": "prepaid",
"feature": {
"id": "premium-messages",
"name": "Premium messages",
"type": "metered",
"display": {
"singular": "premium message",
"plural": "premium messages"
}
},
"quantity": 200,
"price": 10,
"price_per": 100,
"display": {
"primary_text": "200 premium messages",
"secondary_text": "$10 per 100 messages"
}
}
}
],
"product": {
"id": "top-up",
"name": "Top-up",
"group": null,
"env": "sandbox",
"is_add_on": false,
"is_default": false,
"archived": false,
"version": 1,
"created_at": 1766428038264,
"items": [
{
"type": "feature",
"feature_id": "premium-messages",
"feature_type": "prepaid",
"feature": {
"id": "premium-messages",
"name": "Premium messages",
"type": "metered",
"display": {
"singular": "premium message",
"plural": "premium messages"
}
},
"price": 10,
"price_per": 100,
"display": {
"primary_text": "$10 per 100 messages"
}
}
],
"free_trial": null,
"base_variant_id": null,
"scenario": "attach",
"properties": {
"is_free": false,
"is_one_off": true,
"has_trial": false,
"updateable": false
}
},
"total": 20,
"currency": "usd",
"url": "https://checkout.stripe.com/c/pay/.......",
"has_prorations": false
}
\`\`\`
</Expandable>
Once the customer completes the payment, they will have an additional 200 premium messages available to use. You can display to the user by getting balances from the \`customer\` method.
</Step>
</Steps>
---
> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.useautumn.com/llms.txt
`;

View File

@@ -1,149 +0,0 @@
export const pricingPrompt = `## Design your Autumn pricing model
This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and products (plans).
### Step 1: Understand your pricing needs
Before building, consider:
1. What features do you want to offer? (API calls, seats, storage, etc.)
2. What plans do you want? (Free, Pro, Enterprise tiers?)
3. How should usage be measured and limited?
---
## Feature Types
Autumn supports these feature types:
- **single_use**: Consumable resources (API calls, tokens, messages, credits, generations)
- **continuous_use**: Non-consumable resources (seats, workspaces, projects, team members)
- **boolean**: On/off features (advanced analytics, priority support, SSO)
- **credit_system**: A unified credit pool that maps to multiple single_use features
---
## Item Types
Products contain an array of items. There are distinct item patterns:
### 1. Flat Fee (standalone price, no feature)
\`\`\`typescript
{ feature_id: null, price: 13, interval: "month" }
\`\`\`
Customer pays $13/month as a base subscription fee.
### 2. Free Feature Allocation (feature grant, no price)
\`\`\`typescript
{ feature_id: "credits", included_usage: 10000 }
\`\`\`
Customer gets 10,000 credits included.
### 3. Metered/Usage-Based Pricing
\`\`\`typescript
{ feature_id: "credits", included_usage: 10000, price: 0.01, usage_model: "pay_per_use", interval: "month" }
\`\`\`
Customer can use 10,000 credits per month, then pays $0.01 per credit after that.
### 4. Prepaid Credit Purchase (one-time purchase of usage)
\`\`\`typescript
{ feature_id: "credits", price: 10, usage_model: "prepaid", billing_units: 10000 }
\`\`\`
Customer pays $10 once to receive 10,000 credits.
### 5. Tiered Pricing
\`\`\`typescript
{ feature_id: "api_calls", included_usage: 1000, tiers: [{ to: 5000, amount: 0.02 }, { to: "inf", amount: 0.01 }], usage_model: "pay_per_use", interval: "month" }
\`\`\`
Customer gets 1,000 API calls free, then pays $0.02/call up to 5,000, then $0.01/call after that.
### 6. Per-Unit Pricing Structure
For any "per-X" pricing (like "$Y per seat", "$Y per project", "$Y per website"), use this pattern:
\`\`\`typescript
// Base subscription fee
{ feature_id: null, price: 10, interval: "month" }
// Unit allocation
{ feature_id: "seats", included_usage: 1, price: 10, usage_model: "pay_per_use", billing_units: 1 }
\`\`\`
This creates: $10/month base price that includes 1 unit, then $10 per additional unit purchased.
**Always** use this two-item pattern for any per-unit pricing - never use pure per-unit without a base fee.
---
## Guidelines
### Naming Conventions
- Product and Feature IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`)
### Features vs Plan Features
- Features define WHAT can be tracked (e.g., "credits"). Plan features define HOW a feature is granted in a plan (recurring, one-time, free, paid).
- Never create duplicate features for the same underlying resource. For example, "monthly tokens" and "one-time tokens" should be the SAME feature ("tokens"), referenced by different plan items with different intervals.
### Default Plans
- **Never** set \`is_default: true\` for plans with prices. Default plans must be free.
### Enterprise Plans
- Ignore "Enterprise" plans with custom pricing in the config. Custom plans can be created per-customer in the Autumn dashboard.
### Annual Plans
- For annual variants, create a separate plan with annual price interval. Name it \`<plan_name> - Annual\`.
### Currency
- Currency can be changed in the Autumn dashboard under Developer > Stripe.
---
## Example Configuration
\`\`\`typescript
import { feature, plan, item } from "atmn";
// Features
export const messages = feature({
id: "messages",
name: "Messages",
type: "single_use",
});
export const seats = feature({
id: "seats",
name: "Team Seats",
type: "continuous_use",
});
// Plans
export const free = plan({
id: "free",
name: "Free",
is_default: true,
items: [
item({ featureId: "messages", included: 100 }),
item({ featureId: "seats", included: 1 }),
],
});
export const pro = plan({
id: "pro",
name: "Pro",
items: [
item({ featureId: null, price: 29, interval: "month" }),
item({ featureId: "messages", included: 10000, price: 0.01, usage_model: "pay_per_use" }),
item({ featureId: "seats", included: 5, price: 10, usage_model: "pay_per_use", billingUnits: 1 }),
],
});
\`\`\`
---
## Next Steps
Once you've designed your pricing:
1. Update \`autumn.config.ts\` with your features and plans
2. Run \`atmn push\` to sync your configuration to Autumn
3. Test in sandbox mode before going live
For more help: https://discord.gg/atmn (we're very responsive)
For questions about specific functionality or advanced use cases, check out the documentation: https://docs.useautumn.com
Docs: https://docs.useautumn.com/llms.txt`;

View File

@@ -1,346 +0,0 @@
export default `---
name: autumn-accepting-payments
description: |
Adds Autumn payment flow to a codebase including checkout, plan changes, and billing state.
Use this skill when:
- Adding payment or checkout flow with Autumn
- Implementing plan upgrades or downgrades
- Building pricing cards or subscription UI
- Adding billing state to display current plans
- Implementing cancel subscription functionality
- Adding prepaid top-ups or credit purchases
- User wants to "add payments" or "set up billing UI"
---
# Add Autumn Payment Flow
Autumn handles Stripe checkout and plan changes. This skill guides you through adding the payment flow to a codebase for ALL plans in the Autumn configuration.
## Step 1: Detect Integration Type
Check if the codebase already has Autumn set up:
- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted: **Path A: React**
- If there's just an \`Autumn\` client initialized: **Path B: Backend SDK**
Before implementing:
1. Tell the user which path you'll follow before proceeding.
2. Tell them you will be building pricing cards to handle billing flows, and ask for any guidance or input.
---
## Path A: React
### Checkout Flow
Use \`checkout\` from \`useCustomer\`. It returns either a Stripe URL (new customer) or checkout preview data (returning customer with card on file).
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { checkout } = useCustomer();
const data = await checkout({ productId: "pro" });
if (!data.url) {
// Returning customer: show confirmation dialog with result data
// data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle }
}
\`\`\`
After user confirms in your dialog, call \`attach\` to enable the plan (and charge card as needed):
\`\`\`tsx
const { attach } = useCustomer();
await attach({ productId: "pro" });
\`\`\`
### Getting Billing State
Use \`usePricingTable\` to get products with their billing scenario and display state.
\`\`\`tsx
import { usePricingTable } from "autumn-js/react";
function PricingPage() {
const { products } = usePricingTable();
// Each product has: scenario, properties
// scenario: "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel"
}
\`\`\`
### Canceling
Only use this if there is no free plan in the user's Autumn config. If there is a free plan, cancel by attaching the free plan instead.
\`\`\`tsx
const { cancel } = useCustomer();
await cancel({ productId: "pro" });
\`\`\`
---
## Path B: Backend SDK
### Checkout Flow
Payments are a 2-step process:
1. **checkout** - Returns Stripe checkout URL (new customer) or preview data (returning customer)
2. **attach** - Confirms purchase when no URL was returned
**TypeScript:**
\`\`\`typescript
import { Autumn } from "autumn-js";
import type { CheckoutResult, AttachResult } from "autumn-js";
const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY });
// Step 1: Get checkout info
const { data } = await autumn.checkout({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",
}) as { data: CheckoutResult };
if (data.url) {
// New customer: redirect to Stripe
return redirect(data.url);
} else {
// Returning customer: return preview data for confirmation UI
// data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle }
return data;
}
// Step 2: After user confirms (only if no URL)
const { data: attachData } = await autumn.attach({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",
}) as { data: AttachResult };
\`\`\`
**Python:**
\`\`\`python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
# Step 1: Get checkout info
response = await autumn.checkout(
customer_id="user_or_org_id_from_auth",
product_id="pro",
)
if response.url:
# New customer: redirect to Stripe
return redirect(response.url)
else:
# Returning customer: return preview data for confirmation UI
return response
# Step 2: After user confirms
attach_response = await autumn.attach(
customer_id="user_or_org_id_from_auth",
product_id="pro",
)
\`\`\`
### Getting Billing State
Use \`products.list\` with a \`customer_id\` to get products with their billing scenario. **Don't build custom billing state logic.**
**TypeScript:**
\`\`\`typescript
const { data } = await autumn.products.list({
customer_id: "user_or_org_id_from_auth",
});
data.list.forEach((product) => {
const { scenario } = product;
// "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel"
});
\`\`\`
**Python:**
\`\`\`python
response = await autumn.products.list(customer_id="user_or_org_id_from_auth")
for product in response.list:
scenario = product.scenario
\`\`\`
**curl:**
\`\`\`bash
curl https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth \\
-H "Authorization: Bearer $AUTUMN_SECRET_KEY"
\`\`\`
### Canceling
\`\`\`typescript
await autumn.cancel({ customer_id: "...", product_id: "pro" });
\`\`\`
Or attach a free product ID to downgrade.
---
## Common Patterns
### Pricing Button Text
\`\`\`typescript
const SCENARIO_TEXT: Record<string, string> = {
scheduled: "Plan Scheduled",
active: "Current Plan",
renew: "Renew",
upgrade: "Upgrade",
new: "Enable",
downgrade: "Downgrade",
cancel: "Cancel Plan",
};
export const getPricingButtonText = (product: Product): string => {
const { scenario, properties } = product;
const { is_one_off, updateable, has_trial } = properties ?? {};
if (has_trial) return "Start Trial";
if (scenario === "active" && updateable) return "Update";
if (scenario === "new" && is_one_off) return "Purchase";
return SCENARIO_TEXT[scenario ?? ""] ?? "Enable Plan";
};
\`\`\`
### Confirmation Dialog Text
\`\`\`typescript
import type { CheckoutResult, Product } from "autumn-js";
export const getConfirmationTexts = (result: CheckoutResult): { title: string; message: string } => {
const { product, current_product, next_cycle } = result;
const scenario = product.scenario;
const productName = product.name;
const currentProductName = current_product?.name;
const nextCycleDate = next_cycle?.starts_at
? new Date(next_cycle.starts_at).toLocaleDateString()
: undefined;
const isRecurring = !product.properties?.is_one_off;
const CONFIRMATION_TEXT: Record<string, { title: string; message: string }> = {
scheduled: { title: "Already Scheduled", message: "You already have this product scheduled." },
active: { title: "Already Active", message: "You are already subscribed to this product." },
renew: { title: "Renew", message: \`Renew your subscription to \${productName}.\` },
upgrade: { title: \`Upgrade to \${productName}\`, message: \`Upgrade to \${productName}. Your card will be charged immediately.\` },
downgrade: { title: \`Downgrade to \${productName}\`, message: \`\${currentProductName} will be cancelled. \${productName} begins \${nextCycleDate}.\` },
cancel: { title: "Cancel", message: \`Your subscription to \${currentProductName} will end \${nextCycleDate}.\` },
};
if (scenario === "new") {
return isRecurring
? { title: \`Subscribe to \${productName}\`, message: \`Subscribe to \${productName}. Charged immediately.\` }
: { title: \`Purchase \${productName}\`, message: \`Purchase \${productName}. Charged immediately.\` };
}
return CONFIRMATION_TEXT[scenario ?? ""] ?? { title: "Change Subscription", message: "You are about to change your subscription." };
};
\`\`\`
---
## Prepaid Top-ups Reference
Let customers purchase prepaid packages and top-ups. If a user hits a usage limit, they may be willing to purchase a top-up.
These are typically one-time purchases (or less commonly, recurring add-ons) that grant a fixed usage of a feature.
### Purchasing top-ups (React)
\`\`\`tsx
import { useCustomer, CheckoutDialog } from "autumn-js/react";
export default function TopUpButton() {
const { checkout } = useCustomer();
return (
<button
onClick={async () => {
await checkout({
productId: "top_up",
dialog: CheckoutDialog,
options: [{
featureId: "premium_messages",
quantity: 200,
}],
});
}}
>
Buy More Messages
</button>
);
}
\`\`\`
### Purchasing top-ups (Backend)
**TypeScript:**
\`\`\`typescript
const { data } = await autumn.checkout({
customer_id: "user_or_org_id_from_auth",
product_id: "top_up",
options: [{
feature_id: "premium_messages",
quantity: 200,
}],
});
if (data.url) {
// Redirect user to Stripe checkout URL
} else {
// Show purchase preview to user
}
\`\`\`
**Python:**
\`\`\`python
response = await autumn.checkout(
customer_id="user_or_org_id_from_auth",
product_id="top-up",
options=[{
"feature_id": "premium-messages",
"quantity": 200,
}],
)
if response.url:
# Redirect user to Stripe checkout URL
pass
else:
# Show purchase preview to user
pass
\`\`\`
---
## Important Notes
- **The result is \`data.url\`, NOT \`data.checkout_url\`**
- This handles all upgrades, downgrades, renewals, and uncancellations automatically
- Product IDs come from the Autumn configuration
- Pass \`successUrl\` to \`checkout\` to redirect users after payment
- Your Autumn configuration is in \`autumn.config.ts\` in your project root
## Resources
- Documentation: https://docs.useautumn.com
- LLM-friendly docs: https://docs.useautumn.com/llms.txt
`;

View File

@@ -0,0 +1,514 @@
export default `---
name: autumn-billing-page
description: |
Build a billing page and manage subscriptions with Autumn.
Use this skill when the user wants to:
- Display active plans or subscription status
- Show usage balances to customers
- Build a pricing page with upgrade/downgrade buttons
- Implement plan switching (upgrades/downgrades)
- Add cancel/uncancel subscription functionality
- Open the Stripe billing portal
- Display usage history charts
- Add prepaid top-ups or credit purchases
---
# Build Your Billing Page
Software applications typically ship with a billing page. This allows customers to change plan, cancel subscription and view their usage.
> Your Autumn configuration is in \`autumn.config.ts\`. If it doesn't exist, run \`npx atmn init\` to log in and generate the file.
## Step 1: Detect Integration Type
Check if the codebase already has Autumn set up:
- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted: **Path A: React**
- If there's just an \`Autumn\` client initialized: **Path B: Backend SDK**
Before implementing:
1. Tell the user which path you'll follow before proceeding.
2. Tell them you will be building billing page components, and ask for any guidance or input.
---
## Active Plans
Display the plan the user is currently on. Users can have multiple active subscriptions and purchases (e.g., main plan and add-ons).
- **\`subscriptions\`** - Free and paid recurring plans
- **\`purchases\`** - One-off plans (e.g., credit top-ups)
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { data: customer } = useCustomer();
const active = customer?.subscriptions.filter(
(sub) => sub.status === "active"
);
console.log(active?.map((sub) => sub.planId).join(", "));
\`\`\`
### TypeScript
\`\`\`typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({ secretKey: "am_sk_test_xxx" });
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const active = customer.subscriptions?.filter(
(sub) => sub.status === "active"
);
console.log(active?.map((sub) => sub.planId).join(", "));
\`\`\`
### Python
\`\`\`python
from autumn_sdk import Autumn
autumn = Autumn("am_sk_test_xxx")
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
active = [s for s in customer.subscriptions if s.status == "active"]
print([s.plan_id for s in active])
\`\`\`
---
## Usage Balances
Metered features have \`granted\`, \`usage\`, and \`remaining\` fields. Use these to display current usage and remaining balance.
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { data: customer, refetch } = useCustomer();
const messages = customer?.balances.messages;
console.log(\\\`\\\${messages?.remaining} / \\\${messages?.granted}\\\`);
// After tracking usage or changing plans, call refetch() to update balances
await refetch();
\`\`\`
### TypeScript
\`\`\`typescript
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const messages = customer.balances?.messages;
console.log(\\\`\\\${messages?.remaining} / \\\${messages?.granted}\\\`);
\`\`\`
### Python
\`\`\`python
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
messages = customer.balances.get("messages")
print(f"{messages.remaining} / {messages.granted}")
\`\`\`
---
## Customer Eligibility
When building a pricing page, you need to know what each plan means for the current customer -- is it an upgrade, a downgrade, or their current plan? Is a free trial available?
Pass a \`customerId\` when listing plans and each plan will include a \`customerEligibility\` object:
- **\`attachAction\`** -- What happens when this plan is attached: \`"activate"\`, \`"upgrade"\`, \`"downgrade"\`, \`"purchase"\`, or \`"none"\`
- **\`status\`** -- The customer's current relationship to this plan: \`"active"\`, \`"scheduled"\`, or undefined if none
- **\`trialAvailable\`** -- Whether the customer is eligible for the plan's free trial
### React
\`\`\`tsx
import { useListPlans, useCustomer } from "autumn-js/react";
const labels = {
activate: "Subscribe",
upgrade: "Upgrade",
downgrade: "Downgrade",
purchase: "Purchase",
};
const getLabel = (eligibility) => {
if (eligibility?.attachAction === "none") {
return eligibility.status === "scheduled" ? "Plan Scheduled" : "Current plan";
}
if (labels[eligibility?.attachAction]) {
return labels[eligibility.attachAction];
}
return "Get started";
};
export default function PricingPage() {
const { data: plans } = useListPlans();
const { attach } = useCustomer();
return plans?.map((plan) => (
<button
key={plan.id}
disabled={plan.customerEligibility?.attachAction === "none"}
onClick={() => attach({ planId: plan.id, redirectMode: "always" })}
>
{getLabel(plan.customerEligibility)}
</button>
));
}
\`\`\`
The React \`useListPlans\` hook automatically includes customer context from \`AutumnProvider\`, so \`customerEligibility\` is populated on every plan without extra configuration.
### TypeScript
\`\`\`typescript
const { list: plans } = await autumn.plans.list({
customerId: "user_123",
});
for (const plan of plans) {
console.log(plan.name, plan.customerEligibility?.attachAction);
// e.g. "Free" "downgrade", "Pro" "none", "Enterprise" "upgrade"
}
\`\`\`
### Python
\`\`\`python
plans = await autumn.plans.list(customer_id="user_123")
for plan in plans.list:
print(plan.name, plan.customer_eligibility.attach_action)
\`\`\`
---
## Switching Plans
Use \`attach\` to switch between plans. This handles upgrades, downgrades, and new subscriptions.
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export default function UpgradeButton() {
const { attach } = useCustomer();
return (
<button onClick={() => attach({ planId: "pro" })}>
Upgrade to Pro
</button>
);
}
\`\`\`
### TypeScript
\`\`\`typescript
const response = await autumn.billing.attach({
customerId: "user_123",
planId: "pro",
});
redirect(response.paymentUrl);
\`\`\`
### Python
\`\`\`python
response = await autumn.billing.attach(
customer_id="user_123",
plan_id="pro",
)
# Redirect to response.payment_url
\`\`\`
---
## Cancelling a Plan
Cancel a subscription using \`billing.update\` with a \`cancelAction\`.
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { updateSubscription } = useCustomer();
// Cancel at end of billing cycle
await updateSubscription({
planId: "pro",
cancelAction: "cancel_end_of_cycle",
});
\`\`\`
### TypeScript
\`\`\`typescript
await autumn.billing.update({
customerId: "user_123",
planId: "pro",
cancelAction: "cancel_end_of_cycle",
});
\`\`\`
### Python
\`\`\`python
await autumn.billing.update(
customer_id="user_123",
plan_id="pro",
cancel_action="cancel_end_of_cycle",
)
\`\`\`
---
## Uncancelling a Plan
If a subscription has a pending cancellation (when \`canceledAt\` is not null while the subscription is still \`active\`), you can reverse it:
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export default function BillingPage() {
const { data: customer, updateSubscription } = useCustomer();
const cancellingSub = customer?.subscriptions.find(
(sub) => sub.status === "active" && sub.canceledAt !== null
);
return cancellingSub ? (
<button onClick={() => updateSubscription({
planId: cancellingSub.planId,
cancelAction: "uncancel"
})}>
Keep plan
</button>
) : null;
}
\`\`\`
### TypeScript
\`\`\`typescript
const customer = await autumn.customers.getOrCreate({
customerId: "user_123",
});
const cancellingSub = customer.subscriptions?.find(
(sub) => sub.status === "active" && sub.canceledAt !== null
);
if (cancellingSub) {
await autumn.billing.update({
customerId: "user_123",
planId: cancellingSub.planId,
cancelAction: "uncancel",
});
}
\`\`\`
### Python
\`\`\`python
customer = await autumn.customers.get_or_create(
customer_id="user_123"
)
cancelling_sub = next(
(s for s in customer.subscriptions
if s.status == "active" and s.canceled_at is not None),
None,
)
if cancelling_sub:
await autumn.billing.update(
customer_id="user_123",
plan_id=cancelling_sub.plan_id,
cancel_action="uncancel",
)
\`\`\`
---
## Stripe Billing Portal
The Stripe billing portal lets users manage their payment method, view past invoices, and cancel their plan. Enable the billing portal in your Stripe settings first.
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { openCustomerPortal } = useCustomer();
await openCustomerPortal({
returnUrl: "https://your-app.com/billing"
});
\`\`\`
### TypeScript
\`\`\`typescript
const { url } = await autumn.billing.openCustomerPortal({
customerId: "user_123",
returnUrl: "https://your-app.com/billing",
});
redirect(url);
\`\`\`
### Python
\`\`\`python
response = await autumn.billing.open_customer_portal(
customer_id="user_123",
return_url="https://your-app.com/billing",
)
# Redirect to response.url
\`\`\`
---
## Usage History Chart
Autumn provides aggregate time series queries for usage data. Pass the response to a charting library like Recharts.
### React
\`\`\`tsx
import { useAggregateEvents } from "autumn-js/react";
const { list, total } = useAggregateEvents({
featureId: "messages",
range: "30d",
});
// list: [{ period: 1234567890, values: { messages: 42 } }, ...]
// total: { messages: { count: 100, sum: 500 } }
\`\`\`
### TypeScript
\`\`\`typescript
const { list, total } = await autumn.events.aggregate({
customerId: "user_123",
featureId: "messages",
range: "30d",
});
\`\`\`
### Python
\`\`\`python
response = await autumn.events.aggregate(
customer_id="user_123",
feature_id="messages",
range="30d",
)
# response.list, response.total
\`\`\`
---
## Prepaid Top-ups Reference
Let customers purchase prepaid packages and top-ups. If a user hits a usage limit, they may be willing to purchase a top-up. These are typically one-time purchases that grant a fixed amount of usage.
### React
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export default function TopUpButton() {
const { attach } = useCustomer();
return (
<button
onClick={async () => {
await attach({
planId: "top_up",
options: [{
featureId: "messages",
quantity: 200,
}],
});
}}
>
Buy More Messages
</button>
);
}
\`\`\`
### TypeScript
\`\`\`typescript
const response = await autumn.billing.attach({
customerId: "user_or_org_id_from_auth",
planId: "top_up",
options: [{
featureId: "messages",
quantity: 200,
}],
});
if (response.paymentUrl) {
redirect(response.paymentUrl);
}
\`\`\`
### Python
\`\`\`python
response = await autumn.billing.attach(
customer_id="user_or_org_id_from_auth",
plan_id="top_up",
options=[{
"feature_id": "messages",
"quantity": 200,
}],
)
\`\`\`
---
## Important Notes
- This handles all upgrades, downgrades, renewals, and uncancellations automatically
- Plan IDs come from the Autumn configuration
- Your Autumn configuration is in \`autumn.config.ts\` in your project root
**Docs:** https://docs.useautumn.com/llms.txt
`;

View File

@@ -1,334 +0,0 @@
export default `---
name: autumn-creating-customers
description: |
Sets up Autumn billing integration by creating an Autumn customer in a codebase.
Use this skill when the user wants to:
- Set up Autumn billing
- Create an Autumn customer
- Integrate Autumn into their app
- Add billing/entitlements with Autumn
- Configure Autumn SDK
---
# Set up Autumn Billing Integration
Autumn is a billing and entitlements layer over Stripe. This skill guides you through creating an Autumn customer and adding it to a place in the app where it will be automatically created.
## Step 1: Analyze the Codebase
Before making changes, detect:
- **Language**: TypeScript/JavaScript, Python, or other
- **If TS/JS - Framework**: Next.js, React Router, Tanstack Start, Hono, Express, Fastify, or other
- **If TS/JS - React frontend?**: Check for React in package.json
Then ask the user:
1. **Should Autumn customers be individual users, or organizations?**
- **Users (B2C)**: Each user has their own plan and limits
- **Organizations (B2B)**: Plans and limits are shared across an org
2. **Have you created an AUTUMN_SECRET_KEY and added it to .env?**
- Prompt them to create one at: https://app.useautumn.com/dev?tab=api_keys
- Add it to \`.env\` as \`AUTUMN_SECRET_KEY\`
Tell the user what you detected, which path you'll follow, and what you'll be adding Autumn to.
---
## Path A: React + Node.js (Fullstack TypeScript)
Use this path if there's a React frontend with a Node.js backend.
### A1. Install the SDK
Use the package manager already installed (npm, yarn, pnpm, bun):
\`\`\`bash
npm install autumn-js
\`\`\`
### A2. Mount the Handler (Server-Side)
This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn.
#### Next.js (App Router)
\`\`\`typescript
// app/api/autumn/[...all]/route.ts
import { autumnHandler } from "autumn-js/next";
export const { GET, POST } = autumnHandler({
identify: async (request) => {
// Get user/org from your auth provider
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: {
name: session?.user.name,
email: session?.user.email,
},
};
},
});
\`\`\`
#### React Router
\`\`\`typescript
// app/routes/api.autumn.tsx
import { autumnHandler } from "autumn-js/react-router";
export const { loader, action } = autumnHandler({
identify: async (args) => {
const session = await auth.api.getSession({ headers: args.request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
});
// routes.ts - add this route
route("api/autumn/*", "routes/api.autumn.tsx")
\`\`\`
#### Tanstack Start
\`\`\`typescript
// routes/api/autumn.$.ts
import { autumnHandler } from "autumn-js/tanstack";
const handler = autumnHandler({
identify: async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
});
export const Route = createFileRoute("/api/autumn/$")({
server: { handlers: handler },
});
\`\`\`
#### Hono
\`\`\`typescript
import { autumnHandler } from "autumn-js/hono";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
\`\`\`
#### Express
\`\`\`typescript
import { autumnHandler } from "autumn-js/express";
app.use(express.json()); // Must be before autumnHandler
app.use("/api/autumn", autumnHandler({
identify: async (req) => {
const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
\`\`\`
#### Fastify
\`\`\`typescript
import { autumnHandler } from "autumn-js/fastify";
fastify.route({
method: ["GET", "POST"],
url: "/api/autumn/*",
handler: autumnHandler({
identify: async (request) => {
const session = await auth.api.getSession({ headers: request.headers as any });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}),
});
\`\`\`
#### Other Frameworks (Generic Handler)
\`\`\`typescript
import { autumnHandler } from "autumn-js/backend";
// Mount this handler onto the /api/autumn/* path in your backend
const handleRequest = async (request) => {
// Your authentication logic here
const customerId = "user_or_org_id_from_auth";
let body = null;
if (request.method !== "GET") {
body = await request.json();
}
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: request.url,
method: request.method,
body: body,
},
});
return new Response(JSON.stringify(response), {
status: statusCode,
headers: { "Content-Type": "application/json" },
});
};
\`\`\`
### A3. Add the Provider (Client-Side)
Wrap your app with \`AutumnProvider\`:
\`\`\`tsx
import { AutumnProvider } from "autumn-js/react";
export default function RootLayout({ children }) {
return (
<AutumnProvider>
{children}
</AutumnProvider>
);
}
\`\`\`
If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`:
\`\`\`tsx
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
\`\`\`
### A4. Create a Test Customer
Add this hook to any component to verify the integration:
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { customer } = useCustomer();
console.log("Autumn customer:", customer);
\`\`\`
This automatically creates an Autumn customer for new users/orgs.
---
## Path B: Backend Only (Node.js, Python, or Other)
Use this path if there's no React frontend, or you prefer server-side only.
### B1. Install the SDK
**Node.js:**
\`\`\`bash
npm install autumn-js
\`\`\`
**Python:**
\`\`\`bash
pip install autumn-py
\`\`\`
### B2. Initialize the Client
**TypeScript/JavaScript:**
\`\`\`typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
\`\`\`
**Python:**
\`\`\`python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
\`\`\`
### B3. Create a Test Customer
This will GET or CREATE a new customer. Add it when a user signs in or loads the app. Pass in ID from auth provider.
The response returns customer state, used to display billing information client-side. Log the Autumn customer client-side.
**TypeScript:**
\`\`\`typescript
const { data, error } = await autumn.customers.create({
id: "user_or_org_id_from_auth",
name: "Test User",
email: "test@example.com",
});
\`\`\`
**Python:**
\`\`\`python
customer = await autumn.customers.create(
id="user_or_org_id_from_auth",
name="Test User",
email="test@example.com",
)
\`\`\`
**cURL:**
\`\`\`bash
curl -X POST https://api.useautumn.com/customers \\
-H "Authorization: Bearer am_sk_test_xxx" \\
-H "Content-Type: application/json" \\
-d '{"id": "user_or_org_id_from_auth", "name": "Test User", "email": "test@example.com"}'
\`\`\`
### Type Safety
When calling these functions from the client, the SDK exports types for all response objects:
\`\`\`tsx
import type { Customer } from "autumn-js";
\`\`\`
---
## Verification
After setup, report to the user:
1. What stack you detected
2. Which path you followed
3. What files you created/modified
4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard
**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root.
**Documentation:** https://docs.useautumn.com/llms.txt
`;

View File

@@ -1,7 +1,8 @@
export default `---
name: autumn-tracking-metered-usage
name: autumn-gating
description: |
Add usage tracking and feature gating with Autumn SDK. Use this skill when asked to:
Add usage tracking and feature gating with the Autumn SDK.
Use this skill when asked to:
- Add usage tracking or metering
- Implement feature limits or gating
- Check feature access or entitlements
@@ -11,9 +12,14 @@ description: |
- Enforce usage limits server-side
---
# Autumn Usage & Gating
# Checking and Tracking Usage
Autumn tracks feature usage and enforces limits. This skill covers adding usage tracking and gating to a codebase.
Autumn handles your customer's payments and grants them the features defined in your plan configuration. There are 2 functions you need to enforce limits and gating:
- \`check\` for feature access, before allowing a user to do something
- \`track\` the usage in Autumn afterwards (if needed)
> Your Autumn configuration is in \`autumn.config.ts\`. If it doesn't exist, run \`npx atmn init\` to log in and generate the file.
## Step 1: Detect Integration Type
@@ -26,39 +32,15 @@ Report what you detected before proceeding.
---
## Frontend Checks (React Hooks)
## Checking Feature Access
Use frontend checks for **UX only** - showing/hiding features, prompting upgrades. These should NOT be trusted for security.
Check if a user has enough remaining balance before executing an action. The \`feature_id\` used here is defined by you when you create the feature in Autumn.
### Check Feature Access
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export function SendChatMessage() {
const { check, refetch } = useCustomer();
const handleSendMessage = async () => {
const { data } = check({ featureId: "messages" });
if (!data?.allowed) {
alert("You're out of messages");
} else {
// send chatbot message
// then, refresh customer usage data
await refetch();
}
};
}
\`\`\`
---
## Backend Checks (Required for Security)
### Backend Check (Required for Security)
**Always check on the backend** before executing any protected action. Frontend checks can be bypassed.
### TypeScript
**TypeScript:**
\`\`\`typescript
import { Autumn } from "autumn-js";
@@ -67,58 +49,115 @@ const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Check before executing the action
const { data } = await autumn.check({
customer_id: "user_or_org_id_from_auth",
feature_id: "api_calls",
const { allowed } = await autumn.check({
customerId: "user_or_org_id_from_auth",
featureId: "messages",
requiredBalance: 1,
});
if (!data.allowed) {
return { error: "Usage limit reached" };
if (!allowed) {
console.log("User has run out of messages");
return;
}
// Safe to proceed - do the actual work here
const result = await doTheActualWork();
// Track usage after success
await autumn.track({
customer_id: "user_or_org_id_from_auth",
feature_id: "api_calls",
value: 1,
});
return result;
\`\`\`
### Python
**Python:**
\`\`\`python
from autumn import Autumn
from autumn_sdk import Autumn
autumn = Autumn('am_sk_test_xxx')
# Check before executing the action
response = await autumn.check(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls"
feature_id="messages",
required_balance=1,
)
if not response.allowed:
raise HTTPException(status_code=403, detail="Usage limit reached")
\`\`\`
# Safe to proceed - do the actual work here
result = await do_the_actual_work()
**cURL:**
# Track usage after success
\`\`\`bash
curl -X POST 'https://api.useautumn.com/v1/check' \\
-H 'Authorization: Bearer am_sk_test_xxx' \\
-H 'Content-Type: application/json' \\
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "messages",
"required_balance": 1
}'
\`\`\`
You can also use \`check\` to gate boolean features (non-metered features), such as access to "premium AI models".
### Frontend Check (React Hooks - UX Only)
When using React hooks, you have access to the customer object which you can use to display billing data. You can use the client-side \`check\` function to gate features and show paywalls. Permissions are determined by reading the local \`data\` state, so no call to Autumn's API is made.
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export function SendChatMessage() {
const { check, refetch } = useCustomer();
const handleSendMessage = async () => {
const { allowed } = check({ featureId: "messages" });
if (!allowed) {
alert("You're out of messages");
} else {
// Send chatbot message
// Then refresh customer usage data
await refetch();
}
};
}
\`\`\`
---
## Tracking Usage
After the user has successfully used a feature, record the usage in Autumn. This will decrement their balance.
**TypeScript:**
\`\`\`typescript
await autumn.track({
customerId: "user_or_org_id_from_auth",
featureId: "messages",
value: 1,
});
\`\`\`
**Python:**
\`\`\`python
await autumn.track(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls",
value=1
feature_id="messages",
value=1,
)
return result
\`\`\`
**cURL:**
\`\`\`bash
curl -X POST 'https://api.useautumn.com/v1/track' \\
-H 'Authorization: Bearer am_sk_test_xxx' \\
-H 'Content-Type: application/json' \\
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "messages",
"value": 1
}'
\`\`\`
You should always handle access checks and usage tracking server-side for security. Users can manipulate client-side code using devtools.
---
## Key Concepts
@@ -129,18 +168,6 @@ return result
- Feature IDs come from the Autumn configuration
- Current usage and total limit are available from the Customer object
### Displaying Usage Info
\`\`\`tsx
import type { Customer } from "autumn-js";
// Balance is: customer.features.<feature_name>.balance
\`\`\`
**Note:** Autumn configuration is typically in \`autumn.config.ts\` in the project root.
**Docs:** https://docs.useautumn.com/llms.txt
---
## Credit Systems Reference
@@ -174,9 +201,9 @@ export function CheckBasicMessage() {
const { check, refetch } = useCustomer();
const handleCheckAccess = async () => {
const { data } = await check({ featureId: "basic_messages", requiredBalance: 1 });
const { allowed } = check({ featureId: "basic_messages", requiredBalance: 1 });
if (!data?.allowed) {
if (!allowed) {
alert("You've run out of basic message credits");
} else {
// proceed with sending message
@@ -189,13 +216,13 @@ export function CheckBasicMessage() {
#### TypeScript
\`\`\`typescript
const { data } = await autumn.check({
customer_id: "user_or_org_id_from_auth",
feature_id: "basic_messages",
required_balance: 1,
const { allowed } = await autumn.check({
customerId: "user_or_org_id_from_auth",
featureId: "basic_messages",
requiredBalance: 1,
});
if (!data.allowed) {
if (!allowed) {
console.log("User has run out of basic message credits");
return;
}
@@ -219,11 +246,17 @@ if not response.allowed:
\`\`\`typescript
await autumn.track({
customer_id: "user_or_org_id_from_auth",
feature_id: "basic_messages",
customerId: "user_or_org_id_from_auth",
featureId: "basic_messages",
value: 2,
});
\`\`\`
This uses 2 basic messages, which costs 0.02 USD credits.
---
**Note:** Autumn configuration is typically in \`autumn.config.ts\` in the project root.
**Docs:** https://docs.useautumn.com/llms.txt
`;

View File

@@ -7,108 +7,472 @@ description: |
- Creating autumn.config.ts configuration
- Setting up usage-based, subscription, or credit-based pricing
- Configuring features like API calls, seats, storage, or credits
- Understanding Autumn feature types (single_use, continuous_use, boolean, credit_system)
- Understanding Autumn feature types (metered, boolean, credit_system)
- Working with plan items, metered billing, or tiered pricing
---
# Autumn Pricing Model Design
This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and products (plans).
This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and plans.
> **Before starting:** Check for an \`autumn.config.ts\` in the project root. If it doesn't exist, run \`npx atmn init\` to log in and generate the file. If you already have a config you want to modify, run \`atmn pull\` to sync it from Autumn first.
## Step 1: Understand Your Pricing Needs
Before building, consider:
1. What features do you want to offer? (API calls, seats, storage, etc.)
2. What plans do you want? (Free, Pro, Enterprise tiers?)
2. What plans do you want? (Free, Pro, etc.)
3. How should usage be measured and limited?
## Feature Types
---
Autumn supports these feature types:
## Features
| Type | Description | Examples |
|------|-------------|----------|
| \`single_use\` | Consumable resources | API calls, tokens, messages, credits, generations |
| \`continuous_use\` | Non-consumable resources | Seats, workspaces, projects, team members |
| \`boolean\` | On/off features | Advanced analytics, priority support, SSO |
| \`credit_system\` | Unified credit pool that maps to multiple single_use features | Credits redeemable for various actions |
Features define what can be gated, metered, or billed in your app.
## Item Types
### \`feature(config)\`
Products contain an array of items. There are distinct item patterns:
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| \`id\` | string | Yes | Unique identifier used in API calls (\`check\`, \`track\`, etc). |
| \`name\` | string | Yes | Display name shown in the dashboard and billing UI. |
| \`type\` | enum | Yes | \`"boolean"\` \\| \`"metered"\` \\| \`"credit_system"\` |
| \`consumable\` | boolean | For metered | \`true\` = consumed (messages, API calls), \`false\` = ongoing (seats, storage). |
| \`eventNames\` | string[] | No | Event names that trigger this feature. Allows multiple features to respond to a single event. |
| \`creditSchema\` | array | For credit_system | Maps metered features to credit costs. Each entry: \`{ meteredFeatureId, creditCost }\`. |
### 1. Flat Fee (standalone price, no feature)
### Feature Types
**Boolean** -- simple on/off flag:
\`\`\`typescript
{ feature_id: null, price: 13, interval: "month" }
export const sso = feature({
id: 'sso',
name: 'SSO Authentication',
type: 'boolean',
});
\`\`\`
Customer pays $13/month as a base subscription fee.
### 2. Free Feature Allocation (feature grant, no price)
**Metered, consumable** -- used up and replenished (messages, API calls):
\`\`\`typescript
{ feature_id: "credits", included_usage: 10000 }
export const messages = feature({
id: 'messages',
name: 'Messages',
type: 'metered',
consumable: true,
});
\`\`\`
Customer gets 10,000 credits included.
### 3. Metered/Usage-Based Pricing
**Metered, non-consumable** -- ongoing usage (seats, storage):
\`\`\`typescript
{ feature_id: "credits", included_usage: 10000, price: 0.01, usage_model: "pay_per_use", interval: "month" }
export const seats = feature({
id: 'seats',
name: 'Seats',
type: 'metered',
consumable: false,
});
\`\`\`
Customer can use 10,000 credits per month, then pays $0.01 per credit after that.
### 4. Prepaid Credit Purchase (one-time purchase of usage)
**Credit system** -- maps multiple metered features to credit costs:
\`\`\`typescript
{ feature_id: "credits", price: 10, usage_model: "prepaid", billing_units: 10000 }
export const basicModel = feature({
id: 'basic_model',
name: 'Basic Model',
type: 'metered',
consumable: true,
});
export const premiumModel = feature({
id: 'premium_model',
name: 'Premium Model',
type: 'metered',
consumable: true,
});
export const credits = feature({
id: 'credits',
name: 'AI Credits',
type: 'credit_system',
creditSchema: [
{ meteredFeatureId: basicModel.id, creditCost: 1 },
{ meteredFeatureId: premiumModel.id, creditCost: 5 },
],
});
\`\`\`
Customer pays $10 once to receive 10,000 credits.
If you set the price per credit to 1 cent, credits become monetary credits (eg, 5 credits = $0.05 per premium message).
### 5. Tiered Pricing
---
## Plans
Plans combine features with pricing to create your subscription tiers, add-ons, and top-ups.
### \`plan(config)\`
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| \`id\` | string | Yes | Unique identifier used in checkout and subscription APIs. |
| \`name\` | string | Yes | Display name shown in pricing tables and billing. |
| \`price\` | object | No | Base subscription price: \`{ amount, interval }\`. |
| \`items\` | array | No | Array of \`item()\` objects defining what's included. |
| \`autoEnable\` | boolean | No | Automatically assign to new customers. Typically used for free plans. |
| \`addOn\` | boolean | No | Allow purchase alongside other plans (instead of replacing them). |
| \`freeTrial\` | object | No | \`{ durationLength, durationType, cardRequired }\`. |
| \`group\` | string | No | Group related plans. Plans in the same group replace each other on upgrade/downgrade. |
Price intervals: \`"month"\` | \`"quarter"\` | \`"semi_annual"\` | \`"year"\` | \`"one_off"\`
Trial duration types: \`"day"\` | \`"month"\` | \`"year"\`
---
## Plan Items
Plan items define what each plan includes -- usage limits, pricing, and billing behavior.
### \`item(config)\`
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| \`featureId\` | string | Yes | The \`id\` of the feature to include. |
| \`included\` | number | No | Amount included for free. Omit for boolean features. |
| \`unlimited\` | boolean | No | Grant unlimited usage of this feature. |
| \`reset\` | object | No | How often the included amount resets: \`{ interval, intervalCount? }\`. |
| \`price\` | object | No | Pricing for usage beyond the included amount. |
| \`proration\` | object | No | Mid-cycle changes: \`{ onIncrease, onDecrease }\`. |
| \`rollover\` | object | No | Carry unused balance: \`{ max, expiryDurationType, expiryDurationLength }\`. |
Reset intervals: \`"hour"\` | \`"day"\` | \`"week"\` | \`"month"\` | \`"quarter"\` | \`"semi_annual"\` | \`"year"\`
Proration options:
- \`onIncrease\`: \`"prorate"\` | \`"charge_immediately"\`
- \`onDecrease\`: \`"prorate"\` | \`"refund_immediately"\` | \`"no_action"\`
---
## Pricing Patterns
The \`price\` object on a plan item supports different billing models.
### Usage-based -- charge based on actual usage
\`\`\`typescript
{ feature_id: "api_calls", included_usage: 1000, tiers: [{ to: 5000, amount: 0.02 }, { to: "inf", amount: 0.01 }], usage_model: "pay_per_use", interval: "month" }
item({
featureId: seats.id,
included: 5,
price: {
amount: 10,
interval: 'month',
billingMethod: 'usage_based',
billingUnits: 1,
},
})
\`\`\`
Customer gets 1,000 API calls free, then pays $0.02/call up to 5,000, then $0.01/call after that.
### 6. Per-Unit Pricing Structure
For any "per-X" pricing (like "$Y per seat", "$Y per project", "$Y per website"), use this pattern:
### Prepaid -- customer buys a fixed quantity upfront
\`\`\`typescript
// Base subscription fee
{ feature_id: null, price: 10, interval: "month" }
// Unit allocation
{ feature_id: "seats", included_usage: 1, price: 10, usage_model: "pay_per_use", billing_units: 1 }
item({
featureId: credits.id,
price: {
amount: 5,
billingUnits: 100,
billingMethod: 'prepaid',
},
})
\`\`\`
This creates: $10/month base price that includes 1 unit, then $10 per additional unit purchased.
### Tiered -- price changes based on usage volume
**Always** use this two-item pattern for any per-unit pricing - never use pure per-unit without a base fee.
\`\`\`typescript
item({
featureId: apiCalls.id,
price: {
tiers: [
{ to: 1000, amount: 0.01 },
{ to: 10000, amount: 0.008 },
{ to: 'inf', amount: 0.005 },
],
billingMethod: 'usage_based',
interval: 'month',
},
})
\`\`\`
### Price Fields Reference
| Param | Type | Description |
|-------|------|-------------|
| \`amount\` | number | Price per \`billingUnits\`. Mutually exclusive with \`tiers\`. |
| \`tiers\` | array | Tiered pricing. Each entry: \`{ to: number \\| "inf", amount }\`. Mutually exclusive with \`amount\`. |
| \`billingMethod\` | enum | \`"usage_based"\` \\| \`"prepaid"\`. Required. |
| \`interval\` | enum | \`"week"\` \\| \`"month"\` \\| \`"quarter"\` \\| \`"semi_annual"\` \\| \`"year"\`. Omit for one-time charges. |
| \`billingUnits\` | number | Units per price (default 1). Eg, $5 per 100 credits = \`amount: 5, billingUnits: 100\`. |
| \`maxPurchase\` | number | Maximum quantity that can be purchased. |
---
## Common Patterns
### Free Plan with Usage Limits
\`\`\`typescript
export const free = plan({
id: 'free',
name: 'Free',
autoEnable: true,
items: [
item({
featureId: messages.id,
included: 5,
reset: { interval: 'month' },
}),
item({
featureId: seats.id,
included: 1,
}),
],
});
\`\`\`
### Paid Plan with Flat Fee + Overage
\`\`\`typescript
export const pro = plan({
id: 'pro',
name: 'Pro',
price: { amount: 20, interval: 'month' },
items: [
item({
featureId: messages.id,
included: 1000,
reset: { interval: 'month' },
price: {
amount: 0.01,
interval: 'month',
billingMethod: 'usage_based',
},
}),
],
});
\`\`\`
### Per-Unit Pricing (e.g., per seat)
For any "per-X" pricing (like "$Y per seat"), use a base fee + unit allocation:
\`\`\`typescript
export const team = plan({
id: 'team',
name: 'Team',
price: { amount: 10, interval: 'month' },
items: [
item({
featureId: seats.id,
included: 1,
price: {
amount: 10,
interval: 'month',
billingMethod: 'usage_based',
billingUnits: 1,
},
}),
],
});
\`\`\`
This creates: $10/month base price that includes 1 seat, then $10 per additional seat.
### Plan with Free Trial
\`\`\`typescript
export const pro = plan({
id: 'pro',
name: 'Pro',
price: { amount: 20, interval: 'month' },
freeTrial: {
durationLength: 14,
durationType: 'day',
cardRequired: true,
},
items: [
item({ featureId: messages.id, included: 1000, reset: { interval: 'month' } }),
item({ featureId: sso.id }),
],
});
\`\`\`
### Add-on / Top-up (One-time Prepaid)
\`\`\`typescript
export const topUp = plan({
id: 'top_up',
name: 'Message Top-Up',
addOn: true,
items: [
item({
featureId: messages.id,
price: {
amount: 5,
billingUnits: 100,
billingMethod: 'prepaid',
},
}),
],
});
\`\`\`
### Annual Plan Variant
For annual variants, create a separate plan with annual price interval:
\`\`\`typescript
export const proAnnual = plan({
id: 'pro_annual',
name: 'Pro - Annual',
group: 'pro',
price: { amount: 192, interval: 'year' },
items: [
item({ featureId: messages.id, included: 1000, reset: { interval: 'month' } }),
],
});
\`\`\`
---
## Full Example
A complete config with a free plan, a paid plan with a trial, and a credits top-up add-on:
\`\`\`typescript
// autumn.config.ts
import { feature, item, plan } from 'atmn';
// Features
export const messages = feature({
id: 'messages',
name: 'Messages',
type: 'metered',
consumable: true,
});
export const seats = feature({
id: 'seats',
name: 'Seats',
type: 'metered',
consumable: false,
});
export const sso = feature({
id: 'sso',
name: 'SSO',
type: 'boolean',
});
// Plans
export const free = plan({
id: 'free',
name: 'Free',
autoEnable: true,
items: [
item({
featureId: messages.id,
included: 5,
reset: { interval: 'month' },
}),
item({
featureId: seats.id,
included: 1,
}),
],
});
export const pro = plan({
id: 'pro',
name: 'Pro',
price: { amount: 20, interval: 'month' },
freeTrial: {
durationLength: 14,
durationType: 'day',
cardRequired: true,
},
items: [
item({
featureId: messages.id,
included: 1000,
reset: { interval: 'month' },
}),
item({
featureId: seats.id,
included: 5,
price: {
amount: 10,
interval: 'month',
billingMethod: 'usage_based',
billingUnits: 1,
},
}),
item({
featureId: sso.id,
}),
],
});
export const topUp = plan({
id: 'top_up',
name: 'Message Top-Up',
addOn: true,
items: [
item({
featureId: messages.id,
price: {
amount: 5,
billingUnits: 100,
billingMethod: 'prepaid',
},
}),
],
});
\`\`\`
---
## Guidelines
### Start Simple
- If the user describes more than 3 features, start with the 3 most important (prioritize metered features) and ask them to confirm before adding more
- Inform them you kept it simple to start with, but they can add more later
### Disambiguate Pricing Model
- When the user mentions a price for a feature, ask whether it should be **usage-based** (pay as you go, billed at the end of the cycle) or **prepaid** (buy a fixed quantity upfront)
- Don't assume one or the other without asking
### Don't Fabricate Capabilities
- If the user asks about pricing or functionality you're not sure Autumn supports, do NOT make it up or assume it can be done
- Point them to Discord (https://discord.gg/atmn) or docs (https://docs.useautumn.com/llms.txt) instead
### Naming Conventions
- Product and Feature IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`)
- Feature and plan IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`)
### Features vs Plan Features
### Features vs Plan Items
- Features define WHAT can be tracked (e.g., "credits")
- Plan features define HOW a feature is granted in a plan (recurring, one-time, free, paid)
- Plan items define HOW a feature is granted in a plan (recurring, one-time, free, paid)
- Never create duplicate features for the same underlying resource
- Example: "monthly tokens" and "one-time tokens" should be the SAME feature ("tokens"), referenced by different plan items with different intervals
### Default Plans
- **Never** set \`is_default: true\` for plans with prices
- **Never** set \`autoEnable: true\` for plans with prices
- Default plans must be free
### Enterprise Plans
@@ -116,85 +480,18 @@ This creates: $10/month base price that includes 1 unit, then $10 per additional
- Ignore "Enterprise" plans with custom pricing in the config
- Custom plans can be created per-customer in the Autumn dashboard
### Annual Plans
- For annual variants, create a separate plan with annual price interval
- Name it \`<plan_name> - Annual\`
### Currency
- Currency can be changed in the Autumn dashboard under Developer > Stripe
## Example Configuration
## Previewing and Pushing Changes
\`\`\`typescript
import { feature, plan, item } from "atmn";
After updating \`autumn.config.ts\`:
// Features
export const messages = feature({
id: "messages",
name: "Messages",
type: "metered",
consumable: true,
});
export const seats = feature({
id: "seats",
name: "Team Seats",
type: "metered",
consumable: false,
});
// Plans
export const free = plan({
id: "free",
name: "Free",
autoEnable: true,
items: [
item({ featureId: messages.id, included: 100 }),
item({ featureId: seats.id, included: 1 }),
],
});
export const pro = plan({
id: "pro",
name: "Pro",
price: {
amount: 29,
interval: "month",
},
items: [
item({
featureId: seats.id,
included: 5,
price: {
amount: 10,
interval: "month",
billingMethod: "usage_based",
},
}),
item({
featureId: messages.id,
included: 10_000,
entityFeatureId: seats.id,
price: {
amount: 0.01,
interval: "month",
billingMethod: "usage_based",
},
}),
],
});
\`\`\`
## Next Steps
Once you've designed your pricing:
1. Update \`autumn.config.ts\` with your features and plans
2. Run \`atmn preview\` to lint, validate and preview your plans - make sure to show the user the output to ensure they're happy with the results.
3. Run \`atmn push\` to sync your configuration to Autumn
4. Test in sandbox mode before going live
1. **Preview first**: Run \`atmn preview\` to lint, validate and preview your plans. Show the output to the user so they can review the full configuration.
2. **Get confirmation**: Ask the user to review, edit, and confirm the preview output before pushing. Do NOT push until the user explicitly confirms.
3. **Push**: Once the user is happy, run \`atmn push\` to sync the configuration to Autumn.
4. Test in sandbox mode before going live. You can push to production with \`atmn push -p\`.
## Resources

View File

@@ -0,0 +1,340 @@
export default `---
name: autumn-setup
description: |
Sets up Autumn billing integration: installs the SDK, creates a customer, and adds the payment flow.
Use this skill when the user wants to:
- Set up Autumn billing
- Create an Autumn customer
- Integrate Autumn into their app
- Add billing/entitlements with Autumn
- Configure Autumn SDK
- Add payment flow or checkout
---
# Set up Autumn Billing
Autumn is a billing and entitlements layer over Stripe. This skill walks through installing the SDK, creating an Autumn customer, and wiring up the payment flow.
> **Before starting:** Check for an \`autumn.config.ts\` in the project root. If it doesn't exist, run \`npx atmn init\` to log in and generate the file (this saves your API key and syncs your config). Then refer to \`autumn.config.ts\` for your product and feature IDs.
## Step 1: Analyze the Codebase
Before making changes, detect:
- **Language**: TypeScript/JavaScript, Python, or other
- **If TS/JS - Framework**: Next.js, Hono, or other
- **If TS/JS - React frontend?**: Check for React in package.json
- **Customer model**: Look at the auth setup to determine whether customers map to individual users or organizations. Check for org/team/workspace models in the codebase.
If it's clear from the codebase (e.g., there's an org model and team-based auth), state your assumption. If it's ambiguous, ask the user:
> **Should Autumn customers be individual users, or organizations?**
> - **Users (B2C)**: Each user has their own plan and limits
> - **Organizations (B2B)**: Plans and limits are shared across an org
## Step 2: Create a Plan and Confirm
Before writing any integration code, present a short plan to the user covering:
- What stack/framework you detected
- Whether customers are users or orgs (and why you think so)
- Which path you're following (React fullstack vs backend-only)
- Which files you'll create or modify
- Where the handler / provider / customer creation will go
- Where the payment flow will be wired up
Ask the user to **read, edit, and confirm** the plan before proceeding. Do NOT start coding until the user approves.
---
## Path A: React + Node.js (Fullstack TypeScript)
Use this path if there's a React frontend with a Node.js backend.
### A1. Install the SDK
Use the package manager already installed (npm, yarn, pnpm, bun):
\`\`\`bash
npm install autumn-js
\`\`\`
### A2. Mount the Handler (Server-Side)
This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn.
#### Next.js (App Router)
\`\`\`typescript
// app/api/autumn/[...all]/route.ts
import { autumnHandler } from "autumn-js/next";
export const { GET, POST } = autumnHandler({
identify: async (request) => {
// Get user/org from your auth provider
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: {
name: session?.user.name,
email: session?.user.email,
},
};
},
});
\`\`\`
#### Hono
\`\`\`typescript
import { autumnHandler } from "autumn-js/hono";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
\`\`\`
#### Other Frameworks (Generic Handler)
For any framework not listed above, use the generic handler:
\`\`\`typescript
import { autumnHandler } from "autumn-js/backend";
// Mount this handler onto the /api/autumn/* path in your backend
const handleRequest = async (request) => {
const session = await auth.api.getSession({ headers: request.headers });
let body = null;
if (request.method !== "GET") {
body = await request.json();
}
const { statusCode, response } = await autumnHandler({
customerId: session?.user.id,
customerData: {
name: session?.user.name,
email: session?.user.email,
},
request: {
url: request.url,
method: request.method,
body: body,
},
});
return new Response(JSON.stringify(response), {
status: statusCode,
headers: { "Content-Type": "application/json" },
});
};
\`\`\`
### A3. Add the Provider (Client-Side)
Wrap your app with \`AutumnProvider\`:
\`\`\`tsx
import { AutumnProvider } from "autumn-js/react";
export default function RootLayout({ children }) {
return (
<AutumnProvider>
{children}
</AutumnProvider>
);
}
\`\`\`
If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`:
\`\`\`tsx
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
\`\`\`
### A4. Create a Customer
Add this hook to any component. It automatically creates an Autumn customer for new users and fetches existing customer state:
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
const { data } = useCustomer();
console.log("Autumn customer:", data);
\`\`\`
Autumn's customer ID is the same as your internal user or org ID from your auth provider. No need to store any extra IDs.
### A5. Stripe Payment Flow
Call \`attach\` when the customer wants to purchase a plan. This returns a Stripe payment URL. Once they pay, Autumn grants access to the features defined in the plan.
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export default function PurchaseButton() {
const { attach } = useCustomer();
return (
<button
onClick={async () => {
await attach({
planId: "pro",
redirectMode: "always",
});
}}
>
Select Pro Plan
</button>
);
}
\`\`\`
This handles all plan change scenarios (upgrades, downgrades, one-time topups, renewals, etc).
The \`redirectMode: "always"\` flag always returns a payment URL:
- New purchases redirect to Stripe Checkout to enter payment details
- Subsequent charges redirect to an Autumn hosted, one-click confirmation page
---
## Path B: Backend Only (Node.js, Python, or Other)
Use this path if there's no React frontend, or you prefer server-side only.
### B1. Install the SDK
**Node.js:**
\`\`\`bash
npm install autumn-js
\`\`\`
**Python:**
\`\`\`bash
pip install autumn-sdk
\`\`\`
### B2. Initialize the Client
**TypeScript/JavaScript:**
\`\`\`typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
\`\`\`
**Python:**
\`\`\`python
from autumn_sdk import Autumn
autumn = Autumn('am_sk_test_xxx')
\`\`\`
### B3. Create a Customer
When the customer signs up, create an Autumn customer. Autumn will automatically enable any \`autoEnable\` plan (typically Free).
**TypeScript:**
\`\`\`typescript
const customer = await autumn.customers.getOrCreate({
customerId: "user_or_org_id_from_auth",
name: "John Doe",
email: "john@example.com",
});
\`\`\`
**Python:**
\`\`\`python
customer = await autumn.customers.get_or_create(
customer_id="user_or_org_id_from_auth",
name="John Doe",
email="john@example.com",
)
\`\`\`
**cURL:**
\`\`\`bash
curl -X POST https://api.useautumn.com/v1/customers \\
-H "Authorization: Bearer am_sk_test_xxx" \\
-H "Content-Type: application/json" \\
-d '{"customer_id": "user_or_org_id_from_auth", "name": "John Doe", "email": "john@example.com"}'
\`\`\`
Autumn's customer ID is the same as your internal user or org ID from your auth provider. No need to store any extra IDs.
### B4. Stripe Payment Flow
Call \`attach\` when the customer wants to purchase a plan. This returns a Stripe payment URL. Redirect the customer to complete payment.
**TypeScript:**
\`\`\`typescript
const response = await autumn.billing.attach({
customerId: "user_or_org_id_from_auth",
planId: "pro",
redirectMode: "always",
});
redirect(response.paymentUrl);
\`\`\`
**Python:**
\`\`\`python
response = await autumn.billing.attach(
customer_id="user_or_org_id_from_auth",
plan_id="pro",
redirect_mode="always",
)
# Redirect to response.payment_url
\`\`\`
**cURL:**
\`\`\`bash
curl -X POST 'https://api.useautumn.com/v1/attach' \\
-H 'Authorization: Bearer am_sk_test_xxx' \\
-H 'Content-Type: application/json' \\
-d '{
"customer_id": "user_or_org_id_from_auth",
"plan_id": "pro",
"redirect_mode": "always"
}'
\`\`\`
This handles all plan change scenarios (upgrades, downgrades, one-time topups, renewals, etc).
The \`redirectMode: "always"\` flag always returns a payment URL:
- New purchases redirect to Stripe Checkout to enter payment details
- Subsequent charges redirect to an Autumn hosted, one-click confirmation page
---
## Verification
After setup, report to the user:
1. What stack you detected
2. Which path you followed
3. What files you created/modified
4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard
**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root.
**Documentation:** https://docs.useautumn.com/llms.txt
`;

View File

@@ -1,10 +1,10 @@
// Skills are YAML-frontmatter markdown files that follow the SKILLS standard
// for AI coding assistants (Claude, Cursor, OpenCode, etc.)
import autumnCreatingCustomersContent from "./autumn-creating-customers.js";
import autumnAcceptingPaymentsContent from "./autumn-accepting-payments.js";
import autumnBillingPageContent from "./autumn-billing-page.js";
import autumnGatingContent from "./autumn-gating.js";
import autumnModellingPricingPlansContent from "./autumn-modelling-pricing-plans.js";
import autumnTrackingMeteredUsageContent from "./autumn-tracking-metered-usage.js";
import autumnSetupContent from "./autumn-setup.js";
export interface Skill {
id: string;
@@ -15,16 +15,22 @@ export interface Skill {
export const skills: Skill[] = [
{
id: "autumn-creating-customers",
name: "Creating Customers",
description: "Set up Autumn billing integration",
content: autumnCreatingCustomersContent,
id: "autumn-setup",
name: "Setup and Payments",
description: "Install SDK, create customers, and add payment flow",
content: autumnSetupContent,
},
{
id: "autumn-accepting-payments",
name: "Accepting Payments",
description: "Add checkout, plan changes, and billing UI",
content: autumnAcceptingPaymentsContent,
id: "autumn-gating",
name: "Checking and Tracking",
description: "Add usage tracking and feature gating",
content: autumnGatingContent,
},
{
id: "autumn-billing-page",
name: "Build Your Billing Page",
description: "Display billing state, plan switching, and subscriptions",
content: autumnBillingPageContent,
},
{
id: "autumn-modelling-pricing-plans",
@@ -32,12 +38,11 @@ export const skills: Skill[] = [
description: "Design pricing models with autumn.config.ts",
content: autumnModellingPricingPlansContent,
},
{
id: "autumn-tracking-metered-usage",
name: "Tracking Metered Usage",
description: "Add usage tracking and feature gating",
content: autumnTrackingMeteredUsageContent,
},
];
export { autumnCreatingCustomersContent, autumnAcceptingPaymentsContent, autumnModellingPricingPlansContent, autumnTrackingMeteredUsageContent };
export {
autumnSetupContent,
autumnGatingContent,
autumnBillingPageContent,
autumnModellingPricingPlansContent,
};

View File

@@ -1,131 +0,0 @@
import { creditSystemDocs } from "./creditSystemDocs.js";
export const usagePrompt = `## Add Autumn gating and usage tracking
Autumn tracks feature usage and enforces limits. Add usage tracking to this codebase.
### Step 1: Detect my integration type
Check if this codebase already has Autumn set up:
- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted → **React hooks available** (can use for UX)
- Backend SDK should **always** be used to enforce limits server-side
Tell me what you detected before proceeding.
---
## Frontend checks (React hooks)
Use frontend checks for **UX only** - showing/hiding features, prompting upgrades. These should NOT be trusted for security.
### Check feature access
\`\`\`tsx
import { useCustomer } from "autumn-js/react";
export function SendChatMessage() {
const { check, refetch } = useCustomer();
const handleSendMessage = async () => {
const { data } = check({ featureId: "messages" });
if (!data?.allowed) {
alert("You're out of messages");
} else {
//send chatbot message
//then, refresh customer usage data
await refetch();
}
};
}
\`\`\`
---
## Backend checks (required for security)
**Always check on the backend** before executing any protected action. Frontend checks can be bypassed.
### TypeScript
\`\`\`typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Check before executing the action
const { data } = await autumn.check({
customer_id: "user_or_org_id_from_auth",
feature_id: "api_calls",
});
if (!data.allowed) {
return { error: "Usage limit reached" };
}
// Safe to proceed - do the actual work here
const result = await doTheActualWork();
// Track usage after success
await autumn.track({
customer_id: "user_or_org_id_from_auth",
feature_id: "api_calls",
value: 1,
});
return result;
\`\`\`
### Python
\`\`\`python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
# Check before executing the action
response = await autumn.check(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls"
)
if not response.allowed:
raise HTTPException(status_code=403, detail="Usage limit reached")
# Safe to proceed - do the actual work here
result = await do_the_actual_work()
# Track usage after success
await autumn.track(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls",
value=1
)
return result
\`\`\`
---
## Notes
- **Frontend checks** = UX (show/hide UI, display limits) - can be bypassed by users
- **Backend checks** = Security (enforce limits) - required before any protected action
- Pattern: check → do work → track (only track after successful completion)
- Feature IDs come from the Autumn configuration
- Current usage and total limit can be taken from from Customer object and displayed -- see the Customer types from the Autumn SDK
\`\`\`tsx
import type { Customer } from "autumn-js";
//Balance is: customer.features.<feature_name>.balance
\`\`\`
For credit systems, see the end of this message.
**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root.
Docs: https://docs.useautumn.com/llms.txt
<Credit System Documentation>
${creditSystemDocs}
</Credit System Documentation>
`;

View File

@@ -13,9 +13,9 @@ If this file is empty, then you should help the user model their pricing structu
Once a pricing model is either decided upon or already found to exist already continue onwards:
- Begin by helping the user create their first customer in Autumn by loading the 'autumn-creating-customer' skill.
- Then setup accepting payments by loading the 'autumn-accepting-payments' skill.
- Lastly start tracking usage by loading the 'autumn-tracking-usage' skill.`;
- Begin by setting up Autumn in the codebase by loading the 'autumn-setup' skill (install SDK, create customer, payment flow).
- Then build the billing page by loading the 'autumn-billing-page' skill (display plans, switching, cancelling).
- Lastly add usage gating by loading the 'autumn-gating' skill (check access, track usage).`;
interface HandoffStepProps {
step: number;
@@ -193,7 +193,10 @@ export function HandoffStep({
and implement Autumn into your codebase?
</Text>
<Box marginTop={1}>
<SelectMenu items={aiChoiceOptions} onSelect={(item) => handleAiChoice(item.value)} />
<SelectMenu
items={aiChoiceOptions}
onSelect={(item) => handleAiChoice(item.value)}
/>
</Box>
</Box>
</Box>

View File

@@ -19,6 +19,7 @@
"dependencies": {
"@ai-sdk/react": "^3.0.25",
"@autumn/shared": "workspace:*",
"atmn": "workspace:*",
"@better-auth/dash": "catalog:",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",

View File

@@ -109,7 +109,7 @@ interface CodeGroupContentProps
const CodeGroupContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
CodeGroupContentProps
>(({ className, children, ...props }, ref) => {
>(({ className, children, copyText: _copyText, ...props }, ref) => {
return (
<TabsPrimitive.Content
ref={ref}

View File

@@ -55,7 +55,7 @@ export function FeatureSelector({
<CaretDownIcon className="size-3 opacity-50" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuContent align="start" className="z-[200]">
<div className="max-h-60 overflow-y-auto">
{features.length === 0 ? (
<div className="py-4 text-center text-sm text-t4">

View File

@@ -11,17 +11,28 @@ import {
interface SDKSelectorProps {
className?: string;
excludeSDKs?: SDKType[];
}
export function SDKSelector({ className }: SDKSelectorProps) {
export function SDKSelector({ className, excludeSDKs }: SDKSelectorProps) {
const selectedSDK = useSDKStore((s) => s.selectedSDK);
const setSelectedSDK = useSDKStore((s) => s.setSelectedSDK);
const selectedOption = SDK_OPTIONS.find((opt) => opt.value === selectedSDK);
const visibleOptions = excludeSDKs
? SDK_OPTIONS.filter((opt) => !excludeSDKs.includes(opt.value))
: SDK_OPTIONS;
const effectiveSDK = excludeSDKs?.includes(selectedSDK)
? "node"
: selectedSDK;
const selectedOption = visibleOptions.find(
(opt) => opt.value === effectiveSDK,
);
return (
<Select
value={selectedSDK}
value={effectiveSDK}
onValueChange={(v) => setSelectedSDK(v as SDKType)}
>
<SelectTrigger className={cn("min-w-28 h-6", className)}>
@@ -39,7 +50,7 @@ export function SDKSelector({ className }: SDKSelectorProps) {
</SelectValue>
</SelectTrigger>
<SelectContent>
{SDK_OPTIONS.map((option) => (
{visibleOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<img
src={option.icon}

View File

@@ -8,27 +8,28 @@ const CURL_SNIPPETS: Record<string, Snippet> = {
"Generate a secret key in the Autumn dashboard. You'll use this in the Authorization header.",
filename: "terminal",
language: "bash",
code: "# Your secret key: am_sk_...",
code: "# Your secret key: am_sk_test_42424242...",
},
"create-customer": {
id: "create-customer",
title: "Create a customer",
description: "Use the REST API to create a customer.",
description: "Use the REST API to create or get an existing customer.",
filename: "terminal",
language: "bash",
code: `curl -X POST https://api.useautumn.com/v1/customers \\
-H "Authorization: Bearer am_sk_test_42424242" \\
-H "Content-Type: application/json" \\
-d '{
"id": "user_or_org_id_from_auth",
"customer_id": "user_or_org_id_from_auth",
"name": "John Doe",
"email": "john@example.com"
}'`,
},
attach: {
id: "attach",
title: "Attach a product",
description: "Subscribe a customer to a product/plan.",
title: "Attach a plan",
description:
"Subscribe a customer to a plan. Returns a payment URL to redirect to.",
filename: "terminal",
language: "bash",
code: `curl -X POST https://api.useautumn.com/v1/attach \\
@@ -36,41 +37,43 @@ const CURL_SNIPPETS: Record<string, Snippet> = {
-H "Content-Type: application/json" \\
-d '{
"customer_id": "user_or_org_id_from_auth",
"product_id": "pro_plan",
"success_url": "http://localhost:3000"
"plan_id": "pro_plan",
"redirect_mode": "always"
}'`,
},
"billing-state": {
id: "billing-state",
title: "Get billing state",
description: "Get products with their billing scenario for a customer.",
description: "Get plans with their billing scenario for a customer.",
filename: "terminal",
language: "bash",
code: `# Get products with billing scenarios
curl "https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth" \\
-H "Authorization: Bearer am_sk_test_42424242"
code: `curl -X POST https://api.useautumn.com/v1/plans.list \\
-H "Authorization: Bearer am_sk_test_42424242" \\
-H "Content-Type: application/json" \\
-d '{
"customer_id": "user_or_org_id_from_auth"
}'
# Response includes scenario for each product:
# "active" | "upgrade" | "downgrade" | "scheduled" | "new"`,
# Response includes scenario for each plan:
# "active" | "upgrade" | "downgrade" | "new"`,
},
checkout: {
id: "checkout",
title: "Handle checkout",
description:
"Initiate checkout. Returns a Stripe URL for new customers, or preview data for returning customers.",
"Attach a plan with redirect_mode. Returns a Stripe URL for new customers, or a confirmation page for returning customers.",
filename: "terminal",
language: "bash",
code: `# Step 1: Get checkout info
curl -X POST https://api.useautumn.com/v1/checkout \\
code: `curl -X POST https://api.useautumn.com/v1/attach \\
-H "Authorization: Bearer am_sk_test_42424242" \\
-H "Content-Type: application/json" \\
-d '{
"customer_id": "user_or_org_id_from_auth",
"product_id": "pro_plan"
"plan_id": "pro_plan",
"redirect_mode": "always"
}'
# If response has "url" redirect to Stripe
# If no "url" → show confirmation, then call /attach`,
# Response contains "payment_url" to redirect to`,
},
check: {
id: "check",
@@ -84,7 +87,8 @@ curl -X POST https://api.useautumn.com/v1/checkout \\
-H "Content-Type: application/json" \\
-d '{
"customer_id": "user_or_org_id_from_auth",
"feature_id": "api_calls"
"feature_id": "api_calls",
"required_balance": 1
}'`,
},
track: {

View File

@@ -16,108 +16,91 @@ const NODE_SNIPPETS: Record<string, Snippet> = {
"Generate a secret key in the Autumn dashboard and add it to your environment variables.",
filename: ".env",
language: "bash",
code: "AUTUMN_SECRET_KEY=am_sk_...",
code: "AUTUMN_SECRET_KEY=am_sk_test_42424242...",
},
"create-customer": {
id: "create-customer",
title: "Create a customer",
description:
"Use the SDK to create a customer when a user signs up or when needed.",
"Use the SDK to create a customer when a user signs up or when needed. Autumn will auto-enable any free plan.",
filename: "customers.ts",
language: "typescript",
code: `import Autumn from "autumn-js";
code: `import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: "am_sk_test_42424242",
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Create a customer
await autumn.customers.create({
id: "user_or_org_id_from_auth",
const customer = await autumn.customers.getOrCreate({
customerId: "user_or_org_id_from_auth",
name: "John Doe",
email: "john@example.com",
});`,
},
attach: {
id: "attach",
title: "Attach a product",
description: "Subscribe a customer to a product/plan.",
title: "Attach a plan",
description:
"Subscribe a customer to a plan. Returns a payment URL to redirect to.",
filename: "billing.ts",
language: "typescript",
code: `import Autumn from "autumn-js";
code: `import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: "am_sk_test_42424242",
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Attach a product to a customer
await autumn.attach({
const response = await autumn.billing.attach({
customerId: "user_or_org_id_from_auth",
productId: "pro_plan",
successUrl: "http://localhost:3000",
});`,
planId: "pro_plan",
redirectMode: "always",
});
redirect(response.paymentUrl);`,
},
"billing-state": {
id: "billing-state",
title: "Get billing state",
description:
"Use products.list with a customer_id to get products with their billing scenario.",
"Use plans.list with a customerId to get plans with their billing scenario.",
filename: "billing.ts",
language: "typescript",
code: `import Autumn from "autumn-js";
code: `import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: "am_sk_test_42424242",
secretKey: process.env.AUTUMN_SECRET_KEY,
});
const buttonText: Record<string, string> = {
active: "Current Plan",
upgrade: "Upgrade",
downgrade: "Downgrade",
scheduled: "Scheduled",
};
const { data } = await autumn.products.list({
const { list: plans } = await autumn.plans.list({
customerId: "user_or_org_id_from_auth",
});
const products = data.list.map((product) => ({
id: product.id,
name: product.name,
buttonText: buttonText[product.scenario] ?? "Subscribe",
}));`,
for (const plan of plans) {
console.log(plan.name, plan.customerEligibility?.scenario);
// e.g. "Free" "downgrade", "Pro" "active", "Enterprise" "upgrade"
}`,
},
checkout: {
id: "checkout",
title: "Handle checkout",
description:
"Use checkout to initiate payment. Returns a Stripe URL for new customers, or preview data for returning customers.",
"Use attach with redirectMode to handle payments. Returns a Stripe URL for new customers, or a confirmation page for returning customers.",
filename: "billing.ts",
language: "typescript",
code: `import Autumn from "autumn-js";
code: `import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: "am_sk_test_42424242",
secretKey: process.env.AUTUMN_SECRET_KEY,
});
const { data } = await autumn.checkout({
const response = await autumn.billing.attach({
customerId: "user_or_org_id_from_auth",
productId: "pro_plan",
planId: "pro_plan",
redirectMode: "always",
});
if (data.url) {
// New customer → redirect to Stripe
return redirect(data.url);
}
// Returning customer → return preview for confirmation UI
console.log("Preview:", data.product, data.total, data.currency);
// After user confirms:
await autumn.attach({
customerId: "user_or_org_id_from_auth",
productId: "pro_plan",
});`,
// Redirect customer to complete payment or confirm plan change
redirect(response.paymentUrl);`,
},
check: {
id: "check",
@@ -126,21 +109,24 @@ await autumn.attach({
"Verify if a customer can use a feature before allowing access.",
filename: "access.ts",
language: "typescript",
code: `import Autumn from "autumn-js";
code: `import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: "am_sk_test_42424242",
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Check if customer can use a feature
const { data } = await autumn.check({
const { allowed } = await autumn.check({
customerId: "user_or_org_id_from_auth",
featureId: "api_calls",
requiredBalance: 1,
});
if (data.allowed) {
// Allow the action
}`,
if (!allowed) {
console.log("Usage limit reached");
return;
}
// Safe to proceed`,
},
track: {
id: "track",
@@ -148,13 +134,12 @@ if (data.allowed) {
description: "Record usage events to enforce limits and track consumption.",
filename: "usage.ts",
language: "typescript",
code: `import Autumn from "autumn-js";
code: `import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: "am_sk_test_42424242",
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Track usage of a feature
await autumn.track({
customerId: "user_or_org_id_from_auth",
featureId: "api_calls",

View File

@@ -7,7 +7,7 @@ const PYTHON_SNIPPETS: Record<string, Snippet> = {
description: "Add Autumn to your project using pip.",
filename: "terminal",
language: "bash",
code: "pip install autumn-py",
code: "pip install autumn-sdk",
},
"env-setup": {
id: "env-setup",
@@ -16,100 +16,76 @@ const PYTHON_SNIPPETS: Record<string, Snippet> = {
"Generate a secret key in the Autumn dashboard and add it to your environment variables.",
filename: ".env",
language: "bash",
code: "AUTUMN_SECRET_KEY=am_sk_...",
code: "AUTUMN_SECRET_KEY=am_sk_test_42424242...",
},
"create-customer": {
id: "create-customer",
title: "Create a customer",
description:
"Use the SDK to create a customer when a user signs up or when needed.",
"Use the SDK to create a customer when a user signs up or when needed. Autumn will auto-enable any free plan.",
filename: "customers.py",
language: "python",
code: `from autumn import Autumn
code: `from autumn_sdk import Autumn
autumn = Autumn(secret_key="am_sk_test_42424242")
autumn = Autumn("am_sk_test_42424242")
# Create a customer
autumn.customers.create(
id="user_or_org_id_from_auth",
customer = await autumn.customers.get_or_create(
customer_id="user_or_org_id_from_auth",
name="John Doe",
email="john@example.com"
email="john@example.com",
)`,
},
attach: {
id: "attach",
title: "Attach a product",
description: "Subscribe a customer to a product/plan.",
title: "Attach a plan",
description:
"Subscribe a customer to a plan. Returns a payment URL to redirect to.",
filename: "billing.py",
language: "python",
code: `from autumn import Autumn
code: `from autumn_sdk import Autumn
autumn = Autumn(secret_key="am_sk_test_42424242")
autumn = Autumn("am_sk_test_42424242")
# Attach a product to a customer
autumn.attach(
response = await autumn.billing.attach(
customer_id="user_or_org_id_from_auth",
product_id="pro_plan",
success_url="http://localhost:3000"
)`,
plan_id="pro_plan",
redirect_mode="always",
)
# Redirect to response.payment_url`,
},
"billing-state": {
id: "billing-state",
title: "Get billing state",
description:
"Use products.list with a customer_id to get products with their billing scenario.",
"Use plans.list with a customer_id to get plans with their billing scenario.",
filename: "billing.py",
language: "python",
code: `from autumn import Autumn
code: `from autumn_sdk import Autumn
autumn = Autumn(secret_key="am_sk_test_42424242")
autumn = Autumn("am_sk_test_42424242")
button_text = {
"active": "Current Plan",
"upgrade": "Upgrade",
"downgrade": "Downgrade",
"scheduled": "Scheduled",
}
plans = await autumn.plans.list(customer_id="user_or_org_id_from_auth")
response = autumn.products.list(customer_id="user_or_org_id_from_auth")
products = [
{
"id": p.id,
"name": p.name,
"button_text": button_text.get(p.scenario, "Subscribe"),
}
for p in response.list
]`,
for plan in plans.list:
print(plan.name, plan.customer_eligibility.scenario)`,
},
checkout: {
id: "checkout",
title: "Handle checkout",
description:
"Use checkout to initiate payment. Returns a Stripe URL for new customers, or preview data for returning customers.",
"Use attach with redirect_mode to handle payments. Returns a Stripe URL for new customers, or a confirmation page for returning customers.",
filename: "billing.py",
language: "python",
code: `from autumn import Autumn
code: `from autumn_sdk import Autumn
autumn = Autumn(secret_key="am_sk_test_42424242")
autumn = Autumn("am_sk_test_42424242")
response = autumn.checkout(
response = await autumn.billing.attach(
customer_id="user_or_org_id_from_auth",
product_id="pro_plan"
plan_id="pro_plan",
redirect_mode="always",
)
if response.url:
# New customer → redirect to Stripe
return redirect(response.url)
# Returning customer → return preview for confirmation UI
print("Preview:", response.product, response.total, response.currency)
# After user confirms:
autumn.attach(
customer_id="user_or_org_id_from_auth",
product_id="pro_plan"
)`,
# Redirect to response.payment_url`,
},
check: {
id: "check",
@@ -118,19 +94,21 @@ autumn.attach(
"Verify if a customer can use a feature before allowing access.",
filename: "access.py",
language: "python",
code: `from autumn import Autumn
code: `from autumn_sdk import Autumn
autumn = Autumn(secret_key="am_sk_test_42424242")
autumn = Autumn("am_sk_test_42424242")
# Check if customer can use a feature
result = autumn.check(
response = await autumn.check(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls"
feature_id="api_calls",
required_balance=1,
)
if result.allowed:
# Allow the action
pass`,
if not response.allowed:
print("Usage limit reached")
return
# Safe to proceed`,
},
track: {
id: "track",
@@ -138,15 +116,14 @@ if result.allowed:
description: "Record usage events to enforce limits and track consumption.",
filename: "usage.py",
language: "python",
code: `from autumn import Autumn
code: `from autumn_sdk import Autumn
autumn = Autumn(secret_key="am_sk_test_42424242")
autumn = Autumn("am_sk_test_42424242")
# Track usage of a feature
autumn.track(
await autumn.track(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls",
value=1
value=1,
)`,
},
};

View File

@@ -22,13 +22,13 @@ const REACT_SNIPPETS: Record<string, Snippet> = {
"Generate a secret key in the Autumn dashboard and add it to your environment variables.",
filename: ".env",
language: "bash",
code: "AUTUMN_SECRET_KEY=am_sk_42424242",
code: "AUTUMN_SECRET_KEY=am_sk_test_42424242...",
},
"add-provider": {
id: "add-provider",
title: "Add Autumn provider",
description:
"Wrap your app with the AutumnProvider to enable the React hooks. If your server URL is different to your client, you will pass in the backend URL as a prop.",
"Wrap your app with the AutumnProvider to enable the React hooks.",
filename: "layout.tsx",
language: "tsx",
code: `import { AutumnProvider } from "autumn-js/react";
@@ -41,9 +41,7 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<AutumnProvider
backendUrl="http://localhost:8000"
>
<AutumnProvider>
{children}
</AutumnProvider>
</body>
@@ -61,38 +59,37 @@ export default function RootLayout({
code: `import { useCustomer } from "autumn-js/react";
function MyComponent() {
const { customer, isLoading } = useCustomer();
if (isLoading) return <div>Loading...</div>;
const { data } = useCustomer();
return (
<div>
<p>Customer ID: {customer?.id}</p>
<p>Customer: {data?.name}</p>
</div>
);
}`,
},
attach: {
id: "attach",
title: "Attach a product",
description: "Subscribe a customer to a product/plan.",
title: "Attach a plan",
description:
"Subscribe a customer to a plan. Handles upgrades, downgrades, and new subscriptions.",
filename: "billing.tsx",
language: "tsx",
code: `import { useCustomer } from "autumn-js/react";
function UpgradeButton() {
export default function PurchaseButton() {
const { attach } = useCustomer();
const handleUpgrade = async () => {
await attach({
productId: "pro_plan",
successUrl: "http://localhost:3000",
});
};
return (
<button onClick={handleUpgrade}>
Upgrade to Pro
<button
onClick={async () => {
await attach({
planId: "pro_plan",
redirectMode: "always",
});
}}
>
Select Pro Plan
</button>
);
}`,
@@ -101,29 +98,32 @@ function UpgradeButton() {
id: "billing-state",
title: "Get billing state",
description:
"Use usePricingTable to get products with their billing scenario for the current customer.",
"Use useListPlans to get plans with their billing scenario for the current customer.",
filename: "billing-page.tsx",
language: "tsx",
code: `import { usePricingTable } from "autumn-js/react";
code: `import { useListPlans, useCustomer } from "autumn-js/react";
const buttonText: Record<string, string> = {
active: "Current Plan",
active: "Current plan",
upgrade: "Upgrade",
downgrade: "Downgrade",
scheduled: "Scheduled",
new: "Get started",
};
function PricingPage() {
const { products } = usePricingTable();
export default function PricingPage() {
const { data: plans } = useListPlans();
const { attach } = useCustomer();
return (
<>
{products.map((product) => (
<PricingCard
key={product.id}
name={product.name}
buttonText={buttonText[product.scenario] ?? "Subscribe"}
/>
{plans?.map((plan) => (
<button
key={plan.id}
disabled={plan.customerEligibility?.scenario === "active"}
onClick={() => attach({ planId: plan.id })}
>
{buttonText[plan.customerEligibility?.scenario] ?? "Get started"}
</button>
))}
</>
);
@@ -133,66 +133,66 @@ function PricingPage() {
id: "checkout",
title: "Handle checkout",
description:
"Use checkout to initiate payment. It auto-redirects new customers to Stripe. For returning customers, it returns preview data.",
"Use attach with redirectMode to handle payments. New customers go to Stripe Checkout, returning customers see a confirmation page.",
filename: "pricing-card.tsx",
language: "tsx",
code: `import { useCustomer } from "autumn-js/react";
const { checkout, attach } = useCustomer();
export default function UpgradeButton() {
const { attach } = useCustomer();
const handleSelect = async (productId: string) => {
const data = await checkout({ productId });
if (!data.url) {
// Returning customer → show confirmation dialog
console.log("Preview:", data.product, data.total, data.currency);
// Then call attach() to confirm the change
await attach({ productId });
}
};`,
},
"attach-pricing-table": {
id: "attach-pricing-table",
title: "Attach a product",
description:
"Drop in a pre-built pricing table that displays your products and handles checkout.",
filename: "pricing.tsx",
language: "tsx",
code: `import { PricingTable } from "autumn-js/react";
export default function PricingPage() {
return (
<div className="w-full max-w-4xl mx-auto p-8">
<PricingTable
checkoutParams={{
successUrl: "http://localhost:3000",
<button
onClick={async () => {
await attach({
planId: "pro_plan",
redirectMode: "always",
});
}}
/>
</div>
>
Upgrade to Pro
</button>
);
}`,
},
// "attach-pricing-table": {
// id: "attach-pricing-table",
// title: "Attach a plan",
// description:
// "Drop in a pre-built pricing table that displays your plans and handles checkout.",
// filename: "pricing.tsx",
// language: "tsx",
// code: `import { PricingTable } from "autumn-js/react";
//
// export default function PricingPage() {
// return (
// <div className="w-full max-w-4xl mx-auto p-8">
// <PricingTable />
// </div>
// );
// }`,
// },
"attach-custom": {
id: "attach-custom",
title: "Attach a product",
title: "Attach a plan",
description:
"Build your own UI and use the attach function to subscribe customers to products.",
"Build your own UI and use the attach function to subscribe customers to plans.",
filename: "billing.tsx",
language: "tsx",
code: `import { useCustomer } from "autumn-js/react";
function UpgradeButton() {
export default function UpgradeButton() {
const { attach } = useCustomer();
const handleUpgrade = async () => {
await attach({
productId: "pro_plan",
successUrl: "http://localhost:3000",
});
};
return (
<button onClick={handleUpgrade}>
<button
onClick={async () => {
await attach({
planId: "pro_plan",
redirectMode: "always",
});
}}
>
Upgrade to Pro
</button>
);
@@ -200,29 +200,28 @@ function UpgradeButton() {
},
"attach-custom-prepaid": {
id: "attach-custom-prepaid",
title: "Attach a product",
title: "Attach a plan",
description:
"Build your own UI and use the attach function to subscribe customers to products.",
"Build your own UI and use attach with options for prepaid purchases.",
filename: "billing.tsx",
language: "tsx",
code: `import { useCustomer } from "autumn-js/react";
function UpgradeButton() {
export default function TopUpButton() {
const { attach } = useCustomer();
const handleUpgrade = async () => {
return (
<button
onClick={async () => {
await attach({
productId: "pro_plan",
successUrl: "http://localhost:3000",
planId: "pro_plan",
options: [
{ feature_id: "prepaid_feature", quantity: 10 }
{ featureId: "prepaid_feature", quantity: 10 }
],
});
};
return (
<button onClick={handleUpgrade}>
Upgrade to Pro
}}
>
Buy More
</button>
);
}`,
@@ -231,25 +230,26 @@ function UpgradeButton() {
id: "check",
title: "Check feature access",
description:
"Verify if a customer can use a feature before allowing access.",
"Verify if a customer can use a feature before allowing access. Client-side checks are for UX only.",
filename: "access.tsx",
language: "tsx",
code: `import { useCustomer } from "autumn-js/react";
function FeatureButton() {
const { check } = useCustomer();
const { check, refetch } = useCustomer();
const handleAction = async () => {
const { data } = await check({
const { allowed } = check({
featureId: "api_calls",
});
if (!data?.allowed) {
if (!allowed) {
alert("You've reached your limit!");
return;
}
// Proceed with the action
// Proceed with the action, then refresh usage data
await refetch();
};
return <button onClick={handleAction}>Use Feature</button>;
@@ -311,14 +311,8 @@ function getBackendFilename(backend: BackendStack): string {
switch (backend) {
case "nextjs":
return "app/api/autumn/[...all]/route.ts";
case "rr7":
return "app/routes/api.autumn.$.tsx";
case "express":
return "server.js";
case "hono":
return "app.ts";
case "elysia":
return "index.ts";
default:
return "handler.ts";
}
@@ -332,16 +326,10 @@ function getBackendSetupCode(
switch (backend) {
case "nextjs":
return getNextjsSnippet(auth, customerType);
case "express":
return getExpressSnippet(customerType);
case "hono":
return getHonoSnippet(customerType);
case "elysia":
return getElysiaSnippet(customerType);
case "rr7":
return getRR7Snippet(customerType);
return getHonoSnippet(auth, customerType);
default:
return getGeneralSnippet();
return getGenericSnippet(customerType);
}
}
@@ -363,133 +351,26 @@ function getNextjsSnippet(
}
}
const getExpressSnippet = (customerType: CustomerType): string => {
return `import express from "express";
import { autumnHandler } from "autumn-js/backend";
const app = express();
app.use(express.json());
app.use("/api/autumn/*", async (req, res) => {
// Your authentication logic here
const customerId = "${customerType === "user" ? "user_id" : "org_id"}";
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: req.url,
method: req.method,
body: req.body,
},
});
res.status(statusCode).json(response);
});`;
};
const getHonoSnippet = (customerType: CustomerType): string => {
return `import { Hono } from "hono";
import { autumnHandler } from "autumn-js/backend";
const app = new Hono();
app.use("/api/autumn/*", async (c) => {
// Your authentication logic here
const customerId = "${customerType === "user" ? "user_id" : "org_id"}";
let body = null;
if (c.req.method !== "GET") {
body = await c.req.json();
function getHonoSnippet(
auth: AuthProvider,
customerType: CustomerType,
): string {
switch (auth) {
case "betterauth":
return customerType === "user" ? honoBetterAuthUser : honoBetterAuthOrg;
default:
return honoOther(customerType);
}
}
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: c.req.url,
method: c.req.method,
body: body,
},
});
return c.json(response, statusCode);
});`;
};
const getElysiaSnippet = (customerType: CustomerType): string => {
return `import { Elysia } from "elysia";
import { autumnHandler } from "autumn-js/backend";
new Elysia()
.all("/api/autumn/*", async ({ request }) => {
// Your authentication logic here
const customerId = "${customerType === "user" ? "user_id" : "org_id"}";
let body = null;
if (request.method !== "GET") {
body = await request.json();
}
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: request.url,
method: request.method,
body: body,
},
});
return new Response(JSON.stringify(response), {
status: statusCode,
headers: { "Content-Type": "application/json" }
});
})
.listen(3000);`;
};
const getRR7Snippet = (customerType: CustomerType): string => {
return `import { autumnHandler } from "autumn-js/backend";
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
const handler = async (request: Request) => {
// Your authentication logic here
const customerId = "${customerType === "user" ? "user_id" : "org_id"}";
let body = null;
if (request.method !== "GET") {
body = await request.json();
}
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: request.url,
method: request.method,
body: body,
},
});
return new Response(JSON.stringify(response), {
status: statusCode,
headers: { "Content-Type": "application/json" },
});
};
export const loader: LoaderFunction = ({ request }) => handler(request);
export const action: ActionFunction = ({ request }) => handler(request);`;
};
const getGeneralSnippet = (): string => {
const getGenericSnippet = (customerType: CustomerType): string => {
return `import { autumnHandler } from "autumn-js/backend";
// Mount this handler onto the /api/autumn/* path in your backend
const handleRequest = async (request) => {
// Your authentication logic here
const customerId = "user_id_or_org_id";
const customerId = "${customerType === "user" ? "user_id" : "org_id"}";
let body = null;
if (request.method !== "GET") {
@@ -513,7 +394,7 @@ const handleRequest = async (request) => {
};`;
};
// Next.js specific snippets
// Next.js specific snippets (using autumn-js/next adapter)
const nextjsBetterAuthUser = `import { autumnHandler } from "autumn-js/next";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
@@ -645,3 +526,58 @@ export const { GET, POST } = autumnHandler({
};
},
});`;
// Hono specific snippets (using autumn-js/hono adapter)
const honoBetterAuthUser = `import { autumnHandler } from "autumn-js/hono";
import { auth } from "./lib/auth";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
return {
customerId: session?.user.id,
customerData: {
name: session?.user.name,
email: session?.user.email,
},
};
},
}));`;
const honoBetterAuthOrg = `import { autumnHandler } from "autumn-js/hono";
import { auth } from "./lib/auth";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
return {
customerId: session?.session.activeOrganizationId,
customerData: {
name: session?.user.name,
email: session?.user.email,
},
};
},
}));`;
const honoOther = (customerType: CustomerType): string => {
return `import { autumnHandler } from "autumn-js/hono";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
// Your authentication logic here
const customerId = "${customerType === "user" ? "user_id" : "org_id"}";
return {
customerId,
customerData: { name: "", email: "" },
};
},
}));`;
};

View File

@@ -65,15 +65,19 @@ export function getSnippetsForStep({
dynamicParams?: DynamicSnippetParams;
}): Snippet[] {
const stepSnippets = STEP_SNIPPETS[stepId];
// Check/track should always show server-side snippets, even for React users
const effectiveSDK = sdk === "react" && stepId === "usage" ? "node" : sdk;
const snippetIds: SnippetId[] =
sdk === "react"
effectiveSDK === "react"
? stepSnippets.react
: sdk === "curl"
: effectiveSDK === "curl"
? stepSnippets.curl
: stepSnippets.other;
return snippetIds.map((id) =>
getSnippet({ id, sdk, stackConfig, dynamicParams }),
getSnippet({ id, sdk: effectiveSDK, stackConfig, dynamicParams }),
);
}

View File

@@ -25,10 +25,7 @@ const FRONTEND_OPTIONS: StackOption[] = [
const BACKEND_OPTIONS: StackOption[] = [
{ value: "nextjs", label: "Next.js", asset: "/frameworks/nextjs.png" },
{ value: "rr7", label: "RR7", asset: "/frameworks/react-router.svg" },
{ value: "hono", label: "Hono", asset: "/frameworks/hono.png" },
{ value: "express", label: "Express", asset: "/frameworks/express.png" },
{ value: "elysia", label: "Elysia", asset: "/frameworks/elysia.png" },
{ value: "general", label: "Other", asset: "/frameworks/nodejs.svg" },
];

View File

@@ -16,13 +16,7 @@ export type SnippetId =
| "track";
export type FrontendStack = "nextjs" | "rr7" | "vite" | "general";
export type BackendStack =
| "nextjs"
| "express"
| "hono"
| "elysia"
| "rr7"
| "general";
export type BackendStack = "nextjs" | "hono" | "general";
export type AuthProvider = "betterauth" | "supabase" | "clerk" | "other";
export type CustomerType = "user" | "org";
@@ -56,7 +50,7 @@ export interface GetSnippetParams {
dynamicParams?: DynamicSnippetParams;
}
export type StepId = "customer" | "payments" | "usage";
export type StepId = "customer" | "usage";
export const STEP_SNIPPETS: Record<
StepId,
@@ -69,14 +63,10 @@ export const STEP_SNIPPETS: Record<
"backend-setup",
"add-provider",
"create-customer",
"attach",
],
other: ["install", "env-setup", "create-customer"],
curl: ["env-setup", "create-customer"],
},
payments: {
react: ["attach"],
other: ["billing-state", "checkout"],
curl: ["billing-state", "checkout"],
other: ["install", "env-setup", "create-customer", "attach"],
curl: ["env-setup", "create-customer", "attach"],
},
usage: {
react: ["check", "track"],

View File

@@ -14,7 +14,6 @@ import {
stepNeedsStackConfig,
} from "@/lib/snippets";
import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
import { AttachStep } from "./steps/AttachStep";
import { BackendSetupStep } from "./steps/BackendSetupStep";
import { EnvSetupStep } from "./steps/EnvSetupStep";
import { SnippetStep } from "./steps/SnippetStep";
@@ -108,13 +107,7 @@ export function CodeSheet({ stepId, title, description }: CodeSheetProps) {
/>
);
case "attach":
return selectedSDK === "react" ? (
<AttachStep
key={snippet.id}
snippet={snippet}
stepNumber={stepNumber}
/>
) : (
return (
<SnippetStep
key={snippet.id}
snippet={snippet}
@@ -167,7 +160,9 @@ export function CodeSheet({ stepId, title, description }: CodeSheetProps) {
<div className="p-4 pb-0">
<div className="flex items-center justify-between gap-4">
<h2 className="text-main">{title}</h2>
<SDKSelector />
<SDKSelector
excludeSDKs={stepId === "usage" ? ["react"] : undefined}
/>
</div>
<p className="text-t3 text-sm mt-1.5">{description}</p>
</div>

View File

@@ -2,13 +2,12 @@
import { AppEnv } from "@autumn/shared";
import {
ChartBar,
BatteryHighIcon,
CheckCircleIcon,
ClockIcon,
CreditCard,
CubeIcon,
PlugsConnectedIcon,
SparkleIcon,
UserCircle,
} from "@phosphor-icons/react";
import { X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
@@ -60,7 +59,7 @@ const ONBOARDING_STEPS: OnboardingStep[] = [
shortTitle: "Plans",
icon: <CubeIcon size={16} weight="duotone" />,
description:
"Create a plan, set pricing, and add the features customers get with it",
"Create a plan, set pricing, and add the features customers get with this plan",
link: "/quickstart",
linkText: "Go to Quickstart",
waitingFor: "Waiting for plan",
@@ -68,28 +67,18 @@ const ONBOARDING_STEPS: OnboardingStep[] = [
{
id: "customer",
stepId: "customer",
title: "Create a customer",
shortTitle: "Customer",
icon: <UserCircle size={16} weight="duotone" />,
description:
"Start integrating your pricing by creating a customer from your app",
title: "Set up Autumn",
shortTitle: "Integration",
icon: <PlugsConnectedIcon size={16} weight="duotone" />,
description: "Install the SDK, create a customer, and add the payment flow",
waitingFor: "Waiting for customer",
},
{
id: "payments",
stepId: "payments",
title: "Handle payments",
shortTitle: "Payments",
icon: <CreditCard size={16} weight="duotone" />,
description: "Build your billing page and handle payments",
waitingFor: "Waiting for checkout",
},
{
id: "usage",
stepId: "usage",
title: "Limits and gating",
shortTitle: "Gating",
icon: <ChartBar size={16} weight="duotone" />,
title: "Checking and tracking balances",
shortTitle: "Balances",
icon: <BatteryHighIcon size={16} weight="duotone" />,
description:
"Give customers access to the features on their plan, and track usage",
waitingFor: "Waiting for event",
@@ -157,7 +146,7 @@ function StepCard({
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.5 } }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
className="absolute top-0 left-0 bottom-0 w-[515px] px-4 flex gap-6 shrink-0!"
className="absolute top-0 left-0 bottom-0 w-[610px] px-4 flex gap-6 shrink-0!"
>
<div className="flex flex-col justify-center">
<h3 className="font-medium text-sm text-foreground mb-1">
@@ -271,17 +260,17 @@ export function OnboardingGuide() {
if (isLoading) {
return (
<div className="relative overflow-x-clip border-dashed border-b pb-4 mb-2">
<div className="relative overflow-x-clip border-dashed border-b pb-4 mb-2 h-32">
{/* Header skeleton */}
<div className="pr-8 mb-2.75">
<div className="flex items-center gap-2">
<Skeleton className="h-3.5 w-36" />
<Skeleton className="h-4 w-16 rounded-md" />
<Skeleton className="h-3.5 w-16 rounded-md" />
</div>
</div>
{/* Steps skeleton - 4 cards */}
<div className="flex gap-3 items-start w-[700px] shrink-0">
{["flex-[4]", "flex-1", "flex-1", "flex-1"].map((flexClass, i) => (
{/* Steps skeleton - 3 cards */}
<div className="flex gap-3 items-start shrink-0">
{["flex-[4]", "flex-1", "flex-1"].map((flexClass, i) => (
<Skeleton key={i} className={cn("rounded-xl h-21", flexClass)} />
))}
</div>

View File

@@ -7,7 +7,7 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
const REFETCH_INTERVAL = 5000;
const DISMISSED_STORAGE_KEY = "autumn_products_onboarding_dismissed";
export type OnboardingStepId = "plans" | "customer" | "payments" | "usage";
export type OnboardingStepId = "plans" | "customer" | "usage";
interface OnboardingStepStatus {
complete: boolean;
@@ -94,26 +94,6 @@ export const useOnboardingProgress = (): OnboardingProgress => {
},
});
// Payments query (checking for Stripe ID)
const { data: paymentsData, isLoading: paymentsLoading } = useQuery<{
fullCustomers: FullCustomer[];
}>({
queryKey: ["onboarding-payments"],
queryFn: async () => {
const { data } = await axiosInstance.post(
"/customers/all/full_customers",
{ page_size: 50 },
);
return data;
},
refetchInterval: (query) => {
const hasStripeCustomer = query.state.data?.fullCustomers?.some(
(c) => c.processor?.id,
);
return hasStripeCustomer ? false : REFETCH_INTERVAL;
},
});
// Events query
const { data: eventsData, isLoading: eventsLoading } = useQuery<{
rawEvents: { data: unknown[] };
@@ -145,29 +125,24 @@ export const useOnboardingProgress = (): OnboardingProgress => {
return hasPrice && hasFeature;
}) ?? false,
customer: (customersData?.fullCustomers?.length ?? 0) > 0,
payments:
paymentsData?.fullCustomers?.some((c) => c.processor?.id) ?? false,
usage: (eventsData?.rawEvents?.data?.length ?? 0) > 0,
}),
[productsData, customersData, paymentsData, eventsData],
[productsData, customersData, eventsData],
);
const currentStep = useMemo((): OnboardingStepId => {
if (!completedSteps.plans) return "plans";
if (!completedSteps.customer) return "customer";
if (!completedSteps.payments) return "payments";
if (!completedSteps.usage) return "usage";
return "plans";
}, [completedSteps]);
const isLoading =
productsLoading || customersLoading || paymentsLoading || eventsLoading;
const isLoading = productsLoading || customersLoading || eventsLoading;
return {
steps: {
plans: { complete: completedSteps.plans },
customer: { complete: completedSteps.customer },
payments: { complete: completedSteps.payments },
usage: { complete: completedSteps.usage },
},
currentStep,

View File

@@ -142,7 +142,8 @@ export function usePricingAgentChat(options?: UsePricingAgentChatOptions) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { messages, sendMessage, status, addToolOutput, setMessages } = useChat({
const { messages, sendMessage, status, addToolOutput, setMessages } = useChat(
{
transport: new DefaultChatTransport({
api: `${import.meta.env.VITE_BACKEND_URL}/pricing-agent/chat`,
credentials: "include",
@@ -186,7 +187,8 @@ export function usePricingAgentChat(options?: UsePricingAgentChatOptions) {
});
}
},
});
},
);
const handleSubmit = useCallback(
(message: PromptInputMessage) => {

View File

@@ -1,187 +1,29 @@
/**
* Prompts for the onboarding guide steps.
* These are copied to clipboard when users click "Copy prompt".
*
* Edit the .md files in the prompts/ folder directly - no escaping needed!
* Use {{PLACEHOLDER}} syntax for dynamic values.
* These are the CLI skill contents (which already include setup/config instructions).
* The single source of truth lives in packages/atmn/src/prompts/skills/.
*/
import {
type CreditSystemConfig,
type Feature,
FeatureType,
type ProductV2,
UsageModel,
} from "@autumn/shared";
import { useCallback, useMemo } from "react";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import customerPrompt from "./prompts/customer.md?raw";
import paymentsPrompt from "./prompts/payments.md?raw";
import usagePrompt from "./prompts/usage.md?raw";
import { autumnGatingContent, autumnSetupContent } from "atmn/skills";
import { useCallback } from "react";
function stripYamlFrontmatter({ content }: { content: string }): string {
return content.replace(/^---[\s\S]*?---\n*/, "");
}
const ONBOARDING_PROMPTS: Record<string, string> = {
customer: customerPrompt,
payments: paymentsPrompt,
usage: usagePrompt,
customer: stripYamlFrontmatter({ content: autumnSetupContent }),
usage: stripYamlFrontmatter({ content: autumnGatingContent }),
};
/** Check if any product has prepaid items */
function hasPrepaidItems({ products }: { products: ProductV2[] }): boolean {
return products.some((p) =>
p.items.some((item) => item.usage_model === UsageModel.Prepaid),
);
}
/** Check if any feature is a credit system */
function hasCreditSystem({ features }: { features: Feature[] }): boolean {
return features.some((f) => f.type === FeatureType.CreditSystem);
}
function buildAutumnConfig({
products,
features,
}: {
products: ProductV2[];
features: Feature[];
}): string {
if (products.length === 0 && features.length === 0) {
return "(No products or features created yet)";
}
const config = {
products: products.map((p) => ({
id: p.id,
name: p.name,
is_add_on: p.is_add_on,
is_default: p.is_default,
group: p.group,
free_trial: p.free_trial,
items: p.items.map((item) => {
const mappedItem: Record<string, unknown> = {};
// Always include type if present
if (item.type) mappedItem.type = item.type;
// Feature fields
if (item.feature_id !== undefined)
mappedItem.feature_id = item.feature_id;
if (item.feature_type) mappedItem.feature_type = item.feature_type;
if (item.included_usage !== undefined)
mappedItem.included_usage = item.included_usage;
// Price fields
if (item.price !== undefined) mappedItem.price = item.price;
if (item.tiers && item.tiers.length > 0) mappedItem.tiers = item.tiers;
if (item.usage_model) mappedItem.usage_model = item.usage_model;
if (item.billing_units) mappedItem.billing_units = item.billing_units;
// Interval
if (item.interval !== undefined) mappedItem.interval = item.interval;
return mappedItem;
}),
})),
features: features.map((f) => {
const mappedFeature: Record<string, unknown> = {
id: f.id,
name: f.name,
type: f.type,
};
// Include credit schema for credit system features
if (f.type === FeatureType.CreditSystem && f.config) {
const config = f.config as CreditSystemConfig;
if (config.schema && config.schema.length > 0) {
mappedFeature.credit_schema = config.schema.map((item) => ({
metered_feature_id: item.metered_feature_id,
credit_amount: item.credit_amount,
}));
}
}
return mappedFeature;
}),
};
return "```json\n" + JSON.stringify(config, null, 2) + "\n```";
}
// Prepaid options snippets
const TS_OPTIONS_COMMENT = `
// Optional: For prepaid pricing, specify quantities
// options: [{ feature_id: "feature_id", quantity: 100 }]`;
const PY_OPTIONS_COMMENT = `
# Optional: For prepaid pricing, specify quantities
# options=[{"feature_id": "feature_id", "quantity": 100}]`;
const PREPAID_SECTION = `
### Prepaid Pricing
If the product has items with \`usage_model: "prepaid"\`, pass the \`options\` array to specify quantities:
\`\`\`typescript
const { data } = await autumn.checkout({
customer_id: "user_123",
product_id: "credits_pack",
options: [{ feature_id: "credits", quantity: 500 }]
});
\`\`\`
`;
const CREDIT_SYSTEM_NOTE = `
**Credit Systems:** You should only check and track with the underlying metered features (see \`credit_schema\` in the configuration), not the credit system itself. Autumn will automatically map usage and deduct the correct credit amount.
`;
/**
* Hook to get onboarding prompts with dynamic values populated.
* Hook to get onboarding prompts for clipboard copy.
*/
export function useOnboardingPrompt() {
const { products } = useProductsQuery();
const { features } = useFeaturesQuery();
const autumnConfig = useMemo(
() => buildAutumnConfig({ products, features }),
[products, features],
);
const hasPrepaid = hasPrepaidItems({ products });
const hasCredits = hasCreditSystem({ features });
const getPrompt = useCallback(
({ stepId }: { stepId: string }): string => {
let prompt = ONBOARDING_PROMPTS[stepId] ?? "";
// Replace dynamic placeholders
prompt = prompt.replace("{{AUTUMN_CONFIG}}", autumnConfig);
// Replace prepaid options placeholders
if (hasPrepaid) {
prompt = prompt.replace("{{TS_CHECKOUT_OPTIONS}}", TS_OPTIONS_COMMENT);
prompt = prompt.replace("{{TS_ATTACH_OPTIONS}}", TS_OPTIONS_COMMENT);
prompt = prompt.replace("{{PY_CHECKOUT_OPTIONS}}", PY_OPTIONS_COMMENT);
prompt = prompt.replace("{{PY_ATTACH_OPTIONS}}", PY_OPTIONS_COMMENT);
prompt = prompt.replace("{{PREPAID_SECTION}}", PREPAID_SECTION);
} else {
// Remove placeholders if no prepaid items
prompt = prompt.replace("{{TS_CHECKOUT_OPTIONS}}", "");
prompt = prompt.replace("{{TS_ATTACH_OPTIONS}}", "");
prompt = prompt.replace("{{PY_CHECKOUT_OPTIONS}}", "");
prompt = prompt.replace("{{PY_ATTACH_OPTIONS}}", "");
prompt = prompt.replace("{{PREPAID_SECTION}}", "");
}
// Replace credit system note placeholder
if (hasCredits) {
prompt = prompt.replace("{{CREDIT_SYSTEM_NOTE}}", CREDIT_SYSTEM_NOTE);
} else {
prompt = prompt.replace("{{CREDIT_SYSTEM_NOTE}}", "");
}
return prompt;
},
[autumnConfig, hasPrepaid, hasCredits],
({ stepId }: { stepId: string }): string =>
ONBOARDING_PROMPTS[stepId] ?? "",
[],
);
return { getPrompt };

View File

@@ -1,294 +0,0 @@
## Set up Autumn billing integration
Autumn is a billing and entitlements layer over Stripe, which we are adding into this codebase. Your task is to create an Autumn customer by following these steps, and add it to a place in this app where it will be automatically created.
### Step 1: Analyze my codebase
Before making changes, detect:
- Language (TypeScript/JavaScript, Python, or other)
- If TS/JS: Framework (Next.js, React Router, Tanstack Start, Hono, Express, Fastify, or other)
- If TS/JS: Is there a React frontend? (Check for React in package.json)
Also ask me:
**1. Should Autumn customers be individual users, or organizations?**
- Users (B2C): Each user has their own plan and limits
- Organizations (B2B): Plans and limits are shared across an org
**2. Have you created an AUTUMN_SECRET_KEY and added it to .env?**
Please prompt them to create one here: https://app.useautumn.com/dev?tab=api_keys and add it to .env as AUTUMN_SECRET_KEY
Tell me what you detected, which path you'll follow and what you'll be adding autumn to.
---
## Path A: React + Node.js (fullstack TypeScript)
Use this path if there's a React frontend with a Node.js backend.
### A1. Install the SDK
**Use the package manager already installed** -- eg user may be using bun, or pnpm.
```bash
npm install autumn-js
```
### A2. Mount the handler (server-side)
This creates endpoints at `/api/autumn/*` that the React hooks will call. The `identify` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn.
**Next.js (App Router):**
```typescript
// app/api/autumn/[...all]/route.ts
import { autumnHandler } from "autumn-js/next";
export const { GET, POST } = autumnHandler({
identify: async (request) => {
// Get user/org from your auth provider
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: {
name: session?.user.name,
email: session?.user.email,
},
};
},
});
```
**React Router:**
```typescript
// app/routes/api.autumn.tsx
import { autumnHandler } from "autumn-js/react-router";
export const { loader, action } = autumnHandler({
identify: async (args) => {
const session = await auth.api.getSession({ headers: args.request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
});
// routes.ts - add this route
route("api/autumn/*", "routes/api.autumn.tsx")
```
**Tanstack Start:**
```typescript
// routes/api/autumn.$.ts
import { autumnHandler } from "autumn-js/tanstack";
const handler = autumnHandler({
identify: async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
});
export const Route = createFileRoute("/api/autumn/$")({
server: { handlers: handler },
});
```
**Hono:**
```typescript
import { autumnHandler } from "autumn-js/hono";
app.use("/api/autumn/*", autumnHandler({
identify: async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
```
**Express:**
```typescript
import { autumnHandler } from "autumn-js/express";
app.use(express.json()); // Must be before autumnHandler
app.use("/api/autumn", autumnHandler({
identify: async (req) => {
const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}));
```
**Fastify:**
```typescript
import { autumnHandler } from "autumn-js/fastify";
fastify.route({
method: ["GET", "POST"],
url: "/api/autumn/*",
handler: autumnHandler({
identify: async (request) => {
const session = await auth.api.getSession({ headers: request.headers as any });
return {
customerId: session?.user.id, // or session?.org.id for B2B
customerData: { name: session?.user.name, email: session?.user.email },
};
},
}),
});
```
**Other frameworks (generic handler):**
```typescript
import { autumnHandler } from "autumn-js/backend";
// Mount this handler onto the /api/autumn/* path in your backend
const handleRequest = async (request) => {
// Your authentication logic here
const customerId = "user_or_org_id_from_auth";
let body = null;
if (request.method !== "GET") {
body = await request.json();
}
const { statusCode, response } = await autumnHandler({
customerId,
customerData: { name: "", email: "" },
request: {
url: request.url,
method: request.method,
body: body,
},
});
return new Response(JSON.stringify(response), {
status: statusCode,
headers: { "Content-Type": "application/json" },
});
};
```
### A3. Add the provider (client-side)
Wrap your app with AutumnProvider:
```tsx
import { AutumnProvider } from "autumn-js/react";
export default function RootLayout({ children }) {
return (
<AutumnProvider>
{children}
</AutumnProvider>
);
}
```
If your backend is on a different URL (e.g., Vite + separate server), pass `backendUrl`:
```tsx
<AutumnProvider backendUrl={import.meta.env.VITE_BACKEND_URL}>
```
### A4. Create a test customer
Add this hook to any component to verify the integration:
```tsx
import { useCustomer } from "autumn-js/react";
const { customer } = useCustomer();
console.log("Autumn customer:", customer);
```
This automatically creates an Autumn customer for new users/orgs.
---
## Path B: Backend only (Node.js, Python, or other)
Use this path if there's no React frontend, or you prefer server-side only.
### B1. Install the SDK
```bash
# Node.js
npm install autumn-js
# Python
pip install autumn-py
```
### B2. Initialize the client
**TypeScript/JavaScript:**
```typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
```
**Python:**
```python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
```
### B3. Create a test customer
This will GET or CREATE a new customer. Add it when a user signs in or loads the app. Pass in ID from auth provider.
The response returns customer state, used to display billing information client-side. Please console.log the Autumn customer client-side.
**TypeScript:**
```typescript
const { data, error } = await autumn.customers.create({
id: "user_or_org_id_from_auth",
name: "Test User",
email: "test@example.com",
});
```
**Python:**
```python
customer = await autumn.customers.create(
id="user_or_org_id_from_auth",
name="Test User",
email="test@example.com",
)
```
**cURL:**
```bash
curl -X POST https://api.useautumn.com/customers \
-H "Authorization: Bearer am_sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{"id": "user_or_org_id_from_auth", "name": "Test User", "email": "test@example.com"}'
```
When calling these functions from the client, the SDK exports types for all response objects. Use these for type-safe code.
```tsx
import type { Customer } from "autumn-js";
```
---
## Verify
After setup, tell me:
1. What stack you detected
2. Which path you followed
3. What files you created/modified
4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard
Docs: https://docs.useautumn.com/llms.txt

View File

@@ -1,248 +0,0 @@
## Add Autumn payment flow
Autumn handles Stripe checkout and plan changes. Your task is to add the payment flow to this codebase for ALL plans in the Autumn configuration.
### Step 1: Detect my integration type
Check if this codebase already has Autumn set up:
- If there's an `AutumnProvider` and `autumnHandler` mounted → **Path A: React**
- If there's just an `Autumn` client initialized → **Path B: Backend SDK**
Before implementing:
1. Tell me which path you'll follow before proceeding.
2. Tell me that I will be building pricing cards to handle billing flows, and ask for any guidance or any input
---
## Path A: React
### Checkout Flow
Use `checkout` from `useCustomer`. It returns either a Stripe URL (new customer) or checkout preview data (returning customer with card on file).
```tsx
import { useCustomer } from "autumn-js/react";
const { checkout } = useCustomer();
const data = await checkout({ productId: "pro" });
if (!data.url) {
// Returning customer → show confirmation dialog with result data
// data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle }
}
```
After user confirms in your dialog, call `attach` to enable plan (and charge card as needed)
```tsx
const { attach } = useCustomer();
await attach({ productId: "pro" });
```
### Getting Billing State
Use `usePricingTable` to get products with their billing scenario and display state.
```tsx
import { usePricingTable } from "autumn-js/react";
function PricingPage() {
const { products } = usePricingTable();
// Each product has: scenario, properties
// scenario: "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel"
}
```
### Canceling
Only use this if there is no free plan in the user's Autumn config. If there is a free plan, then you can cancel by attaching the free plan.
```tsx
const { cancel } = useCustomer();
await cancel({ productId: "pro" });
```
---
## Path B: Backend SDK
### Checkout Flow
Payments are a 2-step process:
1. **checkout** - Returns Stripe checkout URL (new customer) or preview data (returning customer)
2. **attach** - Confirms purchase when no URL was returned
**TypeScript:**
```typescript
import { Autumn } from "autumn-js";
import type { CheckoutResult, AttachResult } from "autumn-js";
const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY });
// Step 1: Get checkout info
const { data } = await autumn.checkout({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",{{TS_CHECKOUT_OPTIONS}}
}) as { data: CheckoutResult };
if (data.url) {
// New customer → redirect to Stripe
return redirect(data.url);
} else {
// Returning customer → return preview data for confirmation UI
// data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle }
return data;
}
// Step 2: After user confirms (only if no URL)
const { data: attachData } = await autumn.attach({
customer_id: "user_or_org_id_from_auth",
product_id: "pro",{{TS_ATTACH_OPTIONS}}
}) as { data: AttachResult };
```
**Python:**
```python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
# Step 1: Get checkout info
response = await autumn.checkout(
customer_id="user_or_org_id_from_auth",
product_id="pro",{{PY_CHECKOUT_OPTIONS}}
)
if response.url:
# New customer → redirect to Stripe
return redirect(response.url)
else:
# Returning customer → return preview data for confirmation UI
return response
# Step 2: After user confirms
attach_response = await autumn.attach(
customer_id="user_or_org_id_from_auth",
product_id="pro",{{PY_ATTACH_OPTIONS}}
)
```
{{PREPAID_SECTION}}
### Getting Billing State
Use `products.list` with a `customer_id` to get products with their billing scenario. **Don't build custom billing state logic.**
**TypeScript:**
```typescript
const { data } = await autumn.products.list({
customer_id: "user_or_org_id_from_auth",
});
data.list.forEach((product) => {
const { scenario } = product;
// "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel"
});
```
**Python:**
```python
response = await autumn.products.list(customer_id="user_or_org_id_from_auth")
for product in response.list:
scenario = product.scenario
```
**curl:**
```bash
curl https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth \
-H "Authorization: Bearer $AUTUMN_SECRET_KEY"
```
### Canceling
```typescript
await autumn.cancel({ customer_id: "...", product_id: "pro" });
```
Or attach a free product ID to downgrade.
---
## Common Patterns
### Pricing Button Text
```typescript
const SCENARIO_TEXT: Record<string, string> = {
scheduled: "Plan Scheduled",
active: "Current Plan",
renew: "Renew",
upgrade: "Upgrade",
new: "Enable",
downgrade: "Downgrade",
cancel: "Cancel Plan",
};
export const getPricingButtonText = (product: Product): string => {
const { scenario, properties } = product;
const { is_one_off, updateable, has_trial } = properties ?? {};
if (has_trial) return "Start Trial";
if (scenario === "active" && updateable) return "Update";
if (scenario === "new" && is_one_off) return "Purchase";
return SCENARIO_TEXT[scenario ?? ""] ?? "Enable Plan";
};
```
### Confirmation Dialog Text
```typescript
import type { CheckoutResult, Product } from "autumn-js";
export const getConfirmationTexts = (result: CheckoutResult): { title: string; message: string } => {
const { product, current_product, next_cycle } = result;
const scenario = product.scenario;
const productName = product.name;
const currentProductName = current_product?.name;
const nextCycleDate = next_cycle?.starts_at
? new Date(next_cycle.starts_at).toLocaleDateString()
: undefined;
const isRecurring = !product.properties?.is_one_off;
const CONFIRMATION_TEXT: Record<string, { title: string; message: string }> = {
scheduled: { title: "Already Scheduled", message: "You already have this product scheduled." },
active: { title: "Already Active", message: "You are already subscribed to this product." },
renew: { title: "Renew", message: `Renew your subscription to ${productName}.` },
upgrade: { title: `Upgrade to ${productName}`, message: `Upgrade to ${productName}. Your card will be charged immediately.` },
downgrade: { title: `Downgrade to ${productName}`, message: `${currentProductName} will be cancelled. ${productName} begins ${nextCycleDate}.` },
cancel: { title: "Cancel", message: `Your subscription to ${currentProductName} will end ${nextCycleDate}.` },
};
if (scenario === "new") {
return isRecurring
? { title: `Subscribe to ${productName}`, message: `Subscribe to ${productName}. Charged immediately.` }
: { title: `Purchase ${productName}`, message: `Purchase ${productName}. Charged immediately.` };
}
return CONFIRMATION_TEXT[scenario ?? ""] ?? { title: "Change Subscription", message: "You are about to change your subscription." };
};
```
---
## Notes
- **NB: the result is `data.url`, NOT `data.checkout_url`**
- This handles all upgrades, downgrades, renewals, uncancellations automatically
- Product IDs come from the Autumn configuration (below)
- Pass `successUrl` to `checkout` to redirect users after payment
Docs: https://docs.useautumn.com/llms.txt
---
## Current Autumn Configuration
{{AUTUMN_CONFIG}}

View File

@@ -1,125 +0,0 @@
## Add Autumn gating and usage tracking
Autumn tracks feature usage and enforces limits. Add usage tracking to this codebase.
### Step 1: Detect my integration type
Check if this codebase already has Autumn set up:
- If there's an `AutumnProvider` and `autumnHandler` mounted → **React hooks available** (can use for UX)
- Backend SDK should **always** be used to enforce limits server-side
Tell me what you detected before proceeding.
---
## Frontend checks (React hooks)
Use frontend checks for **UX only** - showing/hiding features, prompting upgrades. These should NOT be trusted for security.
### Check feature access
```tsx
import { useCustomer } from "autumn-js/react";
export function SendChatMessage() {
const { check, refetch } = useCustomer();
const handleSendMessage = async () => {
const { data } = check({ featureId: "messages" });
if (!data?.allowed) {
alert("You're out of messages");
} else {
//send chatbot message
//then, refresh customer usage data
await refetch();
}
};
}
```
---
## Backend checks (required for security)
**Always check on the backend** before executing any protected action. Frontend checks can be bypassed.
### TypeScript
```typescript
import { Autumn } from "autumn-js";
const autumn = new Autumn({
secretKey: process.env.AUTUMN_SECRET_KEY,
});
// Check before executing the action
const { data } = await autumn.check({
customer_id: "user_or_org_id_from_auth",
feature_id: "api_calls",
});
if (!data.allowed) {
return { error: "Usage limit reached" };
}
// Safe to proceed - do the actual work here
const result = await doTheActualWork();
// Track usage after success
await autumn.track({
customer_id: "user_or_org_id_from_auth",
feature_id: "api_calls",
value: 1,
});
return result;
```
### Python
```python
from autumn import Autumn
autumn = Autumn('am_sk_test_xxx')
# Check before executing the action
response = await autumn.check(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls"
)
if not response.allowed:
raise HTTPException(status_code=403, detail="Usage limit reached")
# Safe to proceed - do the actual work here
result = await do_the_actual_work()
# Track usage after success
await autumn.track(
customer_id="user_or_org_id_from_auth",
feature_id="api_calls",
value=1
)
return result
```
---
## Notes
- **Frontend checks** = UX (show/hide UI, display limits) - can be bypassed by users
- **Backend checks** = Security (enforce limits) - required before any protected action
- Pattern: check → do work → track (only track after successful completion)
- Feature IDs come from the Autumn configuration (below)
- Current usage and total limit can be taken from from Customer object and displayed -- see the Customer types from the Autumn SDK
```tsx
import type { Customer } from "autumn-js";
//Balance is: customer.features.<feature_name>.balance
```
{{CREDIT_SYSTEM_NOTE}}
Docs: https://docs.useautumn.com/llms.txt
---
## Current Autumn Configuration
{{AUTUMN_CONFIG}}

View File

@@ -9,7 +9,8 @@
"paths": {
"@/*": ["./src/*"],
"autumn-js": ["../packages/autumn-js/src/sdk/index.ts"],
"autumn-js/react": ["../packages/autumn-js/src/react/index.ts"]
"autumn-js/react": ["../packages/autumn-js/src/react/index.ts"],
"atmn/skills": ["../packages/atmn/src/prompts/skills/index.ts"]
}
// "noUnusedLocals": false
}

View File

@@ -34,6 +34,10 @@ export default defineConfig({
__dirname,
"../packages/autumn-js/src/sdk/index.ts",
),
"atmn/skills": path.resolve(
__dirname,
"../packages/atmn/src/prompts/skills/index.ts",
),
// Hide Radix UI imports with cleaner aliases
"@radix/accordion": "@radix-ui/react-accordion",
@@ -56,6 +60,7 @@ export default defineConfig({
// Exclude workspace dependencies from pre-bundling to avoid cache issues
exclude: [
"@autumn/shared",
"atmn/skills",
"autumn-js",
"autumn-js/react",
"better-auth",