logoPay4SaaS
Consumption

Credits & Consumption

Credits are used by Credits mode and Quota Subscription mode. If your product uses Unlimited Subscription or Lifetime mode only, you can skip this page.

How Credits Work

Pay4SaaS supports credits from purchases, monthly quota, and bonus campaigns. The backend decides how credits are combined and deducted, so the frontend only needs to show the available balance and call the provided consumption API.

When a paid action runs, Pay4SaaS uses the most appropriate available credits first and keeps purchased credits for last. If the user has no usable credits left, the paid action is rejected.

useCredits Hook

import { useCredits } from '@/hooks/useCredits'

function CreditsDisplay() {
  const { balance, loading, refreshBalance, useCredit } = useCredits()

  if (loading) return <div>Loading...</div>

  return (
    <div>
      <p>Available credits: {balance.availableCredits}</p>
      <p>Used: {balance.usedCredits}</p>
    </div>
  )
}

Balance Values

Most screens should display availableCredits and, if useful, usedCredits. Avoid designing public UI around internal credit buckets unless your product explicitly needs that breakdown.

Consume Credits

const { useCredit } = useCredits()

async function handleUse() {
  const result = await useCredit(
    'image_generation',      // Service type
    'Generated an image'     // Description, optional
  )

  if (result.success) {
    console.log('Remaining:', result.remainingCredits)
  } else {
    console.log(result.message)
  }
}

useCredit and useService both call the same backend consumption flow. Use useCredit when the screen is mainly about credits, and use useService when the screen is a paid feature that should also refresh access status.

The deduction amount is decided on the server by SERVICE_COSTS in config/payment.ts. Do not send trusted prices or credit costs from the frontend.

Refreshing Balance

After a successful credit purchase, a bonus claim, or a screen transition where you need the latest balance, call refreshBalance():

const { refreshBalance } = useCredits()

await refreshBalance()

For instant UI feedback after a known successful local action, you can update the displayed balance optimistically:

const { addCredits } = useCredits()

addCredits(50)

Only use optimistic updates for display. The server remains the source of truth for real access and deduction.

Server-Side Credit Operations

For custom API routes or server actions, use the payment helpers instead of writing directly to database tables:

import {
  grantPurchasedCreditsToUser,
  addBonusCredits,
  refreshUserCreditBalance,
} from '@/lib/payment/credits'
import { getBonusExpiryDate } from '@/config/payment'

await grantPurchasedCreditsToUser(userId, 50)

const grant = await addBonusCredits(userId, 10, getBonusExpiryDate(30))
if (!grant.success) {
  console.log(grant.error)
}

const balance = await refreshUserCreditBalance(userId)

These helpers keep credit granting, expiry, and balance refresh behavior consistent with the rest of the template.

Bonus Credits

Bonus credits are suitable for sign-up rewards, promotional campaigns, subscription perks, and manual customer support grants.

The demo bonus button is for delivery preview only. In production, bonus credits should come from an explicit business rule, such as a one-time claim campaign, sign-up reward, payment event, admin panel, or scheduled job.

Claim Campaigns

Configure user-claimable bonus campaigns in config/payment.ts:

export const BONUS_CAMPAIGNS = {
  hero_welcome_bonus: {
    amount: 10,
    expiresInDays: 7,
    oncePerUser: true,
    enabled: true,
  },
}

The frontend only sends the campaign key. The backend decides the amount, expiry, and duplicate-claim rule:

const { claimBonus } = useAccess()

const res = await claimBonus('hero_welcome_bonus')

Manual Grants

For backend-only grants, call addBonusCredits() from an API route or server action:

import { addBonusCredits } from '@/lib/payment/credits'
import { getBonusExpiryDate } from '@/config/payment'

await addBonusCredits(userId, 20, getBonusExpiryDate(30))
await addBonusCredits(userId, 50, getBonusExpiryDate(7), 'campaign')

Deduction Safety

Credit deduction is handled atomically on the backend. Multiple requests cannot spend the same credit balance twice, and every successful deduction is recorded for later usage history.

Docs home

Return to the full implementation guide.

Pricing

Review subscriptions, credits, and lifetime options.

Blog

Read more notes on SaaS payments and growth.

On this page