600 lines
14 KiB
Plaintext
600 lines
14 KiB
Plaintext
---
|
|
title: One-time top ups
|
|
description: Let customers purchase a prepaid package to top up their balance when it falls low.
|
|
---
|
|
|
|
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 (but can also be 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 messages per month for free
|
|
- An option for customers to top-up messages in packages of $10 per 100 messages.
|
|
|
|
## Configure Pricing
|
|
|
|
<Steps>
|
|
<Step>
|
|
#### Create Features
|
|
|
|
Create a `metered` `consumable` feature for our messages, so we can track their balance.
|
|
|
|
<Frame>
|
|
<img src="/assets/guides/prepaid/features-light.png" className="block dark:hidden" />
|
|
<img src="/assets/guides/prepaid/features-dark.png" className="hidden dark:block" />
|
|
</Frame>
|
|
</Step>
|
|
|
|
<Step>
|
|
#### Create Free and Top-up Plans
|
|
|
|
**Free Plan** <br />
|
|
Create a free plan, and assign 10 messages to it. We'll add an interval of "month", so that the user is granted 10 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="/assets/guides/prepaid/free-light.png" className="block dark:hidden" />
|
|
<img src="/assets/guides/prepaid/free-dark.png" className="hidden dark:block" />
|
|
</Frame>
|
|
|
|
**Top up Plan** <br />
|
|
Now we'll create our top-up plan. Again, we'll assign the messages feature, but this time with a `prepaid` price of $10 per 100 messages.
|
|
|
|
Since these messages have interval "one-off", the messages will last forever (unlike our Free plan messages, which reset every month).
|
|
|
|
Features with a `prepaid` price require a `quantity` to be passed in when a customer purchases the plan, so the customer can specify how many messages they want to top up with.
|
|
|
|
<Frame>
|
|
<img src="/assets/guides/prepaid/topup-light.png" className="block dark:hidden" />
|
|
<img src="/assets/guides/prepaid/topup-dark.png" className="hidden dark:block" />
|
|
</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 the 10 monthly 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
|
|
|
|
Before our user sends a message, we'll first check if they have a balance of 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: "messages" });
|
|
|
|
if (!data?.allowed) {
|
|
alert("You've run out of messages");
|
|
} else {
|
|
// proceed with sending message
|
|
await refetch();
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
```typescript Node.js
|
|
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: "messages",
|
|
});
|
|
|
|
if (!data.allowed) {
|
|
console.log("User has run out of messages");
|
|
return;
|
|
}
|
|
```
|
|
|
|
```python Python
|
|
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="messages",
|
|
)
|
|
|
|
if not response.allowed:
|
|
print("User has run out of messages")
|
|
return
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
```bash cURL
|
|
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": "messages"
|
|
}'
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
<Expandable title="check response">
|
|
```json
|
|
{
|
|
"customer_id": "user_or_org_id_from_auth",
|
|
"feature_id": "messages",
|
|
"code": "feature_found",
|
|
"allowed": true,
|
|
"balance": 10,
|
|
"usage": 0,
|
|
"included_usage": 10,
|
|
"unlimited": false,
|
|
"interval": null,
|
|
"interval_count": 1,
|
|
"next_reset_at": 2803498203,
|
|
"overage_allowed": false
|
|
}
|
|
```
|
|
</Expandable>
|
|
</Step>
|
|
|
|
<Step>
|
|
#### Tracking messages used
|
|
|
|
After the user has used a message, record it in Autumn to decrease their remaining balance. In this example, the user used 5 messages.
|
|
|
|
<CodeGroup>
|
|
|
|
```typescript Node.js
|
|
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: "messages",
|
|
value: 5,
|
|
});
|
|
```
|
|
|
|
```python Python
|
|
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="messages",
|
|
value=5,
|
|
)
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
```bash cURL
|
|
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": "messages",
|
|
"value": 5
|
|
}'
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
<Expandable title="track response">
|
|
```json
|
|
{
|
|
"code": "event_received",
|
|
"customer_id": "user_or_org_id_from_auth",
|
|
"feature_id": "messages"
|
|
}
|
|
```
|
|
</Expandable>
|
|
</Step>
|
|
|
|
<Step>
|
|
#### Purchasing top-ups
|
|
|
|
When users run out of 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
|
|
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: "messages",
|
|
quantity: 200,
|
|
}],
|
|
});
|
|
}}
|
|
>
|
|
Buy More Messages
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
```typescript Node.js
|
|
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: "messages",
|
|
quantity: 200,
|
|
}],
|
|
});
|
|
|
|
if (data.url) {
|
|
// Redirect user to Stripe checkout URL
|
|
} else {
|
|
// Show purchase preview to user
|
|
}
|
|
```
|
|
|
|
```python Python
|
|
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": "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
|
|
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": "messages",
|
|
"quantity": 200
|
|
}]
|
|
}'
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
<Expandable title="checkout response">
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
</Step>
|
|
<Step>
|
|
#### Displaying balances to the user
|
|
You can display to the user by getting balances from the `customer` method. Under the `customer.features` record, you'll be able to retrieve a current balance, total granted, and a `breakdown` of their monthly vs top-up messages.
|
|
|
|
<CodeGroup>
|
|
|
|
```jsx React [expandable]
|
|
import { useCustomer } from "autumn-js/react";
|
|
|
|
const { customer } = useCustomer();
|
|
|
|
const messages = customer?.features?.messages;
|
|
|
|
// Get breakdown of monthly vs prepaid balances
|
|
const monthlyBalance = messages?.breakdown?.find(
|
|
(b) => b.interval === "month"
|
|
);
|
|
const prepaidBalance = messages?.breakdown?.find(
|
|
(b) => b.interval === "lifetime"
|
|
);
|
|
|
|
// Display both balances to the user
|
|
return (
|
|
<div>
|
|
<p>Monthly: {monthlyBalance?.balance ?? 0} remaining</p>
|
|
<p>Prepaid: {prepaidBalance?.balance ?? 0} remaining</p>
|
|
<p>Total: {messages?.balance ?? 0} messages available</p>
|
|
</div>
|
|
);
|
|
```
|
|
|
|
```typescript Node.js [expandable]
|
|
import { Autumn } from "autumn-js";
|
|
|
|
const autumn = new Autumn({
|
|
secretKey: 'am_sk_42424242',
|
|
});
|
|
|
|
const { data } = await autumn.customer({
|
|
customer_id: "user_or_org_id_from_auth",
|
|
});
|
|
|
|
const messages = data?.features?.messages;
|
|
|
|
// Extract monthly vs prepaid balances from the breakdown
|
|
const monthlyBalance = messages?.breakdown?.find(
|
|
(b) => b.interval === "month"
|
|
);
|
|
const prepaidBalance = messages?.breakdown?.find(
|
|
(b) => b.interval === "lifetime"
|
|
);
|
|
|
|
console.log(`Monthly: ${monthlyBalance?.balance ?? 0} remaining`);
|
|
console.log(`Prepaid: ${prepaidBalance?.balance ?? 0} remaining`);
|
|
console.log(`Total: ${messages?.balance ?? 0} messages available`);
|
|
```
|
|
|
|
```python Python [expandable]
|
|
import asyncio
|
|
from autumn import Autumn
|
|
|
|
autumn = Autumn("am_sk_42424242")
|
|
|
|
async def main():
|
|
response = await autumn.customer(
|
|
customer_id="user_or_org_id_from_auth",
|
|
)
|
|
|
|
messages = response.features.get("messages", {})
|
|
breakdown = messages.get("breakdown", [])
|
|
|
|
# Extract monthly vs prepaid balances
|
|
monthly = next((b for b in breakdown if b.get("interval") == "month"), None)
|
|
prepaid = next((b for b in breakdown if b.get("interval") == "lifetime"), None)
|
|
|
|
print(f"Monthly: {monthly['balance'] if monthly else 0} remaining")
|
|
print(f"Prepaid: {prepaid['balance'] if prepaid else 0} remaining")
|
|
print(f"Total: {messages.get('balance', 0)} messages available")
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
```bash cURL
|
|
curl -X GET "https://api.useautumn.com/v1/customers/user_or_org_id_from_auth" \
|
|
-H "Authorization: Bearer am_sk_42424242" \
|
|
-H "Content-Type: application/json"
|
|
|
|
# get features usage object from customer.features.[feature_id]
|
|
```
|
|
|
|
</CodeGroup>
|
|
|
|
<Expandable title="customer response">
|
|
```json
|
|
{
|
|
"features": {
|
|
"messages": {
|
|
"id": "messages",
|
|
"type": "single_use",
|
|
"name": "Messages",
|
|
"interval": "multiple",
|
|
"interval_count": null,
|
|
"unlimited": false,
|
|
"balance": 205,
|
|
"usage": 5,
|
|
"included_usage": 210,
|
|
"next_reset_at": null,
|
|
"overage_allowed": false,
|
|
"breakdown": [
|
|
{
|
|
"interval": "month",
|
|
"interval_count": 1,
|
|
"balance": 5,
|
|
"usage": 5,
|
|
"included_usage": 10,
|
|
"next_reset_at": 1772191445539,
|
|
"overage_allowed": false
|
|
},
|
|
{
|
|
"interval": "lifetime",
|
|
"interval_count": 1,
|
|
"balance": 200,
|
|
"usage": 0,
|
|
"included_usage": 200,
|
|
"next_reset_at": null,
|
|
"overage_allowed": false
|
|
}
|
|
]
|
|
}
|
|
},
|
|
}
|
|
```
|
|
</Expandable>
|
|
</Step>
|
|
</Steps>
|