Set Up Usage-Based Billing with Prepaid Credits
With Freemius, you can easily sell AI credits or set up usage-based billing for other consumptive features in your SaaS product.
You can sell different subscription plans that provide a specific number of credits per month.
However, you may also want to sell credit top-ups through one-time purchases. This is where Freemius usage-based billing with prepaid credits comes in.
If you're building a SaaS product with a similar setup using Next.js, you can watch our video tutorial for a complete walkthrough. The code is also available in our GitHub repository.
Configuring Your Plans
We will configure the plans using two strategies:
- A set of plans with subscription-only pricing. These plans provide continued access to your SaaS product's features and may optionally include recurring monthly credits (e.g., 100 credits per month).
- A dedicated plan for credit top-ups. We will configure this plan with multi-unit pricing so that users can purchase different quantities of credits.
If you do not wish to have subscription plans, you can skip the first step and just set up the credit top-up plan.
Subscription-Only Plans
For each subscription plan, set only the annual and/or monthly price under Billing Cycles.
To avoid confusion, ensure that these plans do not have a One-off Price.
Credit Top-Up Plan
Create a new plan named Credit Topup. Under the plan's pricing, click Add Bulk to set prices for bulk credits.
In the example below, we have set the price for 1,000 credits as $8, 5,000 credits as $30, and 10,000 credits as $50.
After you set multi-unit pricing, the plan UI changes from Billing Cycles to unit-based pricing. The plan is now a multi-unit plan, allowing users to purchase different quantities of credits. For each pricing option, set only the One-off Price and leave the monthly and annual prices empty.
By default, the unit label is set to Unit. You can change it to Credit or another label that makes sense for your SaaS product by following our guide.
Modify the Entitlement Logic
In our basic SaaS Integration guide, we explained how to set up the purchase data processing logic for active subscriptions.
We will use the same database table and logic to maintain the user's credit balance.
Assume that the user's current balance is stored in a credits column in the users table. After the user makes a purchase, you can update their credit balance from the same function:
- JS SDK
- PHP
import { type PurchaseInfo } from '@freemius/sdk';
import { db } from './db'; // our database instance
// A map of pricing id with the number of credits it represents
const pricingToResourceMap: Record<string, keyof typeof resourceRecord> = {
// The pricing ids are generated by Freemius and you can either put them here or in your .env file.
[process.env.FREEMIUS_PRICING_ID_TOPUP_1000!]: 1000,
[process.env.FREEMIUS_PRICING_ID_TOPUP_5000!]: 5000,
[process.env.FREEMIUS_PRICING_ID_TOPUP_10000!]: 10000,
};
// A new function add credits
export async function addCredits(
userId: string,
credits: number
): Promise<void> {
await db.user.update({
where: { id: userId },
data: { credit: { increment: credits } },
});
}
export async function processPurchaseInfo(purchase: PurchaseInfo) {
const user = await findUserByEmail(purchase.email);
// We exit if the user hasn't registered to our application yet.
// Alternatively, you can register the user automatically here if desired.
if (!user) {
return;
}
// Convert the insertion into a transaction to find out if the entitlement already exists
await db.transaction(async (tx) => {
const isExisting = await tx.userFsEntitlement.findUnique({
where: {
fsLicenseId: purchase.licenseId,
},
});
if (isExisting) {
// Just update the existing entitlement record with the new purchase info
await tx.userFsEntitlement.update({
where: {
fsLicenseId: purchase.licenseId,
},
data: purchase.toEntitlementRecord(),
});
} else {
// New purchase, so add it and also update the user's credit balance based on the pricing id
const creditsToAdd = pricingToResourceMap[purchase.pricingId];
await addCredits(user.id, creditsToAdd);
}
});
}
function addCredits(string $user_id, int $credits): void {
db()->user
->where('id', $user_id)
->increment('credits', $credits);
}
function processPurchaseInfo(string $license_id): void {
$pricing_to_credit_map = [
getenv('FREEMIUS_PRICING_ID_TOPUP_1000') => 1000,
getenv('FREEMIUS_PRICING_ID_TOPUP_5000') => 5000,
getenv('FREEMIUS_PRICING_ID_TOPUP_10000') => 10000,
];
// @see https://freemius.com/help/api/licenses/retrieve/
$license = getLicenseFromFreemiusApi($license_id);
// @see https://freemius.com/help/api/users/retrieve/
$user = getUserFromFreemiusApi($license['user_id']);
// @see https://freemius.com/help/api/licenses/retrieve-latest-subscription/
$subscription = getSubscriptionFromFreemiusApi($license_id);
$local_user = findUserByEmail($user['email']);
// We exit if the user hasn't registered to our application yet.
// Alternatively, you can register the user automatically here if desired.
if (!$local_user) {
return;
}
$credits_to_add = $pricing_to_credit_map[$license['pricing_id']] ?? null;
if (is_null($credits_to_add)) {
throw new UnexpectedValueException('Unknown Freemius pricing ID.');
}
db()->transaction(function () use (
$credits_to_add,
$license,
$local_user,
$subscription,
$user
): void {
$existing_entitlement = db()->user_fs_entitlement
->where('fs_license_id', $license['id'])
->first();
// Insert or update the purchase in our local database.
db()->user_fs_entitlement->updateOrInsert(
['fs_license_id' => $license['id']],
[
'user_id' => $local_user->id,
'fs_plan_id' => $license['plan_id'],
'fs_pricing_id' => $license['pricing_id'],
'fs_user_id' => $user['id'],
'type' => is_null($subscription) ? 'lifetime' : 'subscription',
'expiration' => $license['expiration'],
'is_canceled' => $license['is_canceled'],
]
);
// Add credits only when processing the purchase for the first time.
if (!$existing_entitlement) {
addCredits($local_user->id, $credits_to_add);
}
});
}
The code above performs the following steps:
- Checks the
user_fs_entitlementtable to see if the purchase already exists. - If it exists, it updates the existing record with the new purchase info.
- If it does not exist, the purchase has not been processed before, so it adds the record and updates the user's credit balance based on the purchase's pricing ID.
The pricing ID is mapped to the number of credits it represents in the pricingToResourceMap object. You can either hardcode the pricing IDs here or store them in your .env file.
Consuming Credits
Credit consumption must be handled in your application logic. For example, if you have a metered feature that consumes credits, you can check the user's credit balance before allowing access to it. For this purpose, we recommend creating utility functions.
- JS SDK
- PHP
export async function getCredits(userId: string): Promise<number> {
const user = await db.user.findUniqueOrThrow({ where: { id: userId } });
return user.credit;
}
export async function hasCredits(
userId: string,
credits: number = 1
): Promise<boolean> {
const creditsAvailable = await getCredits(userId);
return creditsAvailable >= credits;
}
export async function deductCredits(
userId: string,
credits: number
): Promise<User | null> {
const updatedUser = await db.user.update({
where: { id: userId },
data: { credit: { decrement: credits } },
});
return updatedUser;
}
function getCredits(string $user_id): int {
$user = db()->user
->where('id', $user_id)
->first();
if (!$user) {
throw new UnexpectedValueException('User not found.');
}
return (int) $user->credits;
}
function hasCredits(string $user_id, int $credits = 1): bool {
$credits_available = getCredits($user_id);
return $credits_available >= $credits;
}
function deductCredits(string $user_id, int $credits): ?User {
return db()->user
->where('id', $user_id)
->decrement('credits', $credits);
}
You can call these functions in your API routes or backend logic to check whether the user has enough credits before allowing access to a metered feature, then deduct the credits after the feature is used.
UI for Purchasing Credits
You can use our React Starter Kit to display the credit top-up plan in your SaaS application. The component will automatically determine which plan is a multi-unit plan and display the pricing accordingly.
Follow these steps:
- Set up the Checkout endpoint using our JavaScript SDK.
- Use the
Topupcomponent from the React Starter Kit to display the credit top-up plan in your SaaS application.
Alternatively, you can use our Checkout Integration guide to implement a custom checkout flow for purchasing credits.