* 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>
537 lines
13 KiB
Plaintext
537 lines
13 KiB
Plaintext
---
|
|
title: "Build your billing page"
|
|
description: "Display usage and billing data in your app for your users"
|
|
---
|
|
|
|
Software applications typically ship with a billing page. This allows customers to change plan, cancel subscription and view their usage.
|
|
|
|
The customer endpoint returns the current state of the customer, including their active subscriptions, one-time purchases, and feature balances.
|
|
|
|
## Pricing table
|
|
|
|
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:
|
|
|
|
| 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 expandable
|
|
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>
|
|
));
|
|
}
|
|
```
|
|
|
|
```typescript TypeScript
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
|
|
|
|
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
|
|
from autumn_sdk import Autumn
|
|
|
|
autumn = Autumn("am_sk_test_1234")
|
|
|
|
plans = await autumn.plans.list(customer_id="user_123")
|
|
|
|
for plan in plans.list:
|
|
print(plan.name, plan.customer_eligibility.attach_action)
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X POST 'https://api.useautumn.com/v1/plans.list' \
|
|
-H 'Authorization: Bearer am_sk_test_1234' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{ "customer_id": "user_123" }'
|
|
|
|
# Each plan in the response includes customer_eligibility
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
<Tip>
|
|
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 uses `billing.attach`. See [Attaching Plans](/documentation/customers/payment-flow) for the full guide.
|
|
|
|
<CodeGroup>
|
|
|
|
```jsx React
|
|
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
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
|
|
|
|
const response = await autumn.billing.attach({
|
|
customerId: "user_123",
|
|
planId: "pro",
|
|
});
|
|
|
|
// Redirect to complete payment or confirm plan change
|
|
redirect(response.paymentUrl);
|
|
```
|
|
|
|
```python Python
|
|
from autumn_sdk import Autumn
|
|
|
|
autumn = Autumn("am_sk_test_1234")
|
|
|
|
response = await autumn.billing.attach(
|
|
customer_id="user_123",
|
|
plan_id="pro",
|
|
)
|
|
# Redirect to response.payment_url
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X POST 'https://api.useautumn.com/v1/attach' \
|
|
-H 'Authorization: Bearer am_sk_test_1234' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"customer_id": "user_123",
|
|
"plan_id": "pro"
|
|
}'
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
## 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.
|
|
|
|
<CodeGroup>
|
|
|
|
```jsx React
|
|
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
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
|
|
|
|
// Cancel at end of billing cycle
|
|
await autumn.billing.update({
|
|
customerId: "user_123",
|
|
planId: "pro",
|
|
cancelAction: "cancel_end_of_cycle",
|
|
});
|
|
```
|
|
|
|
```python Python
|
|
from autumn_sdk import Autumn
|
|
|
|
autumn = Autumn("am_sk_test_1234")
|
|
|
|
# Cancel at end of billing cycle
|
|
await autumn.billing.update(
|
|
customer_id="user_123",
|
|
plan_id="pro",
|
|
cancel_action="cancel_end_of_cycle",
|
|
)
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X POST 'https://api.useautumn.com/v1/billing/update' \
|
|
-H 'Authorization: Bearer am_sk_test_1234' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"customer_id": "user_123",
|
|
"plan_id": "pro",
|
|
"cancel_action": "cancel_end_of_cycle"
|
|
}'
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
## 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"`.
|
|
|
|
A subscription is pending cancellation when `canceledAt` is not null while the subscription is still `active`.
|
|
|
|
<CodeGroup>
|
|
|
|
```jsx React
|
|
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
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
|
|
|
|
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
|
|
from autumn_sdk import Autumn
|
|
|
|
autumn = Autumn("am_sk_test_1234")
|
|
|
|
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",
|
|
)
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X POST 'https://api.useautumn.com/v1/billing/update' \
|
|
-H 'Authorization: Bearer am_sk_test_1234' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"customer_id": "user_123",
|
|
"plan_id": "pro",
|
|
"cancel_action": "uncancel"
|
|
}'
|
|
```
|
|
|
|
</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.
|
|
|
|
<Warning>
|
|
Enable the billing portal in your [Stripe settings](https://dashboard.stripe.com/settings/billing/portal).
|
|
</Warning>
|
|
|
|
<CodeGroup>
|
|
|
|
```jsx React
|
|
import { useCustomer } from "autumn-js/react";
|
|
|
|
const { openCustomerPortal } = useCustomer();
|
|
|
|
// Opens Stripe billing portal in current tab
|
|
await openCustomerPortal({
|
|
returnUrl: "https://your-app.com/billing"
|
|
});
|
|
```
|
|
|
|
```typescript TypeScript
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
|
|
|
|
const { url } = await autumn.billing.openCustomerPortal({
|
|
customerId: "user_123",
|
|
returnUrl: "https://your-app.com/billing",
|
|
});
|
|
|
|
redirect(url);
|
|
```
|
|
|
|
```python Python
|
|
from autumn_sdk import Autumn
|
|
|
|
autumn = Autumn("am_sk_test_1234")
|
|
|
|
response = await autumn.billing.open_customer_portal(
|
|
customer_id="user_123",
|
|
return_url="https://your-app.com/billing",
|
|
)
|
|
# Redirect to response.url
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X POST 'https://api.useautumn.com/v1/billing.open_customer_portal' \
|
|
-H 'Authorization: Bearer am_sk_test_1234' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"customer_id": "user_123",
|
|
"return_url": "https://your-app.com/billing"
|
|
}'
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
|
|
## Usage timeseries chart
|
|
|
|
Autumn provides aggregate time series queries for usage data. Pass the response to a charting library like Recharts.
|
|
|
|
<CodeGroup>
|
|
|
|
```jsx React
|
|
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
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
|
|
|
|
const { list, total } = await autumn.events.aggregate({
|
|
customerId: "user_123",
|
|
featureId: "messages",
|
|
range: "30d",
|
|
});
|
|
```
|
|
|
|
```python Python
|
|
from autumn_sdk import Autumn
|
|
|
|
autumn = Autumn("am_sk_test_1234")
|
|
|
|
response = await autumn.events.aggregate(
|
|
customer_id="user_123",
|
|
feature_id="messages",
|
|
range="30d",
|
|
)
|
|
# response.list, response.total
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X POST 'https://api.useautumn.com/v1/events.aggregate' \
|
|
-H 'Authorization: Bearer am_sk_test_1234' \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"customer_id": "user_123",
|
|
"feature_id": "messages",
|
|
"range": "30d"
|
|
}'
|
|
```
|
|
</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.
|