In today's API economy, choosing the right monetization strategy can make or break your business. While subscription-based pricing remains popular, many companies are discovering the advantages of selling API credits – a flexible approach that lets customers prepay for usage and consume services at their own pace.

Think of API credits as a digital currency specific to your service. Instead of charging customers based on calendar months or user seats, you let them purchase credits upfront and deduct them based on API calls or specific actions. Companies like OpenAI and Google Cloud have popularized this model, but it's equally effective for businesses of any size.

In this guide, we'll explore why credit-based pricing might be the right choice for your API business and walk through a practical implementation using Laravel and Stripe. You'll learn how to set up the entire system – from selling credit packages to tracking usage in your application.

Why Consider Credit-Based Pricing?

Transitioning to credit-based pricing isn't just about following a trend – it offers tangible benefits for both your business and your customers.

For your business, prepaid credits mean improved cash flow and reduced payment processing overhead. Instead of dealing with hundreds of micro-transactions, you process fewer, larger payments. This often results in lower transaction fees and simpler accounting.

Your customers benefit from volume discounts when buying credits in bulk and gain better usage control. They can purchase credits during budget-flush periods and use them throughout the year without additional procurement cycles. This is particularly valuable for enterprise customers who prefer predictable costs and streamlined purchasing.

Consider credit-based pricing if:

  • Your API has variable usage patterns across customers
  • You want to incentivize bulk purchases
  • Your customers need spending controls and clear usage visibility
  • You're looking to reduce payment processing costs
  • Your service has a clear per-action value (like sending emails or processing images)

Business Considerations Before Implementation

Before diving into code, you need to make several critical business decisions that will affect your implementation:

  1. Credit Package Sizing
    Start with packages that match your customers' typical monthly usage. For example, if customers usually spend around $100/month, consider packages of $250, $500, and $1000 to encourage slight overbuying.
  2. Credit Value
    Define how many API calls or actions one credit represents. Keep it simple – one credit could equal one API call, or 1000 credits could equal $1 worth of usage. The simpler this conversion, the easier it is for customers to understand.
  3. Expiration Rules
    While credits typically don't expire, you might want to implement expiration for promotional credits or to ensure regular account activity. Be transparent about any expiration policies.
  4. Overage Handling
    Decide whether to allow usage when credits run low. You might want to stop service, auto-purchase more credits, or switch to pay-as-you-go pricing.

Technical Implementation

Let's implement a simple credit system in Laravel that handles purchases and usage tracking. We'll start with the database schema and then move to the purchase flow.

First, create migrations for storing credits and their usage:

// database/migrations/2025_01_24_create_credit_packages_table.php
public function up(): void
{
    Schema::create('credit_packages', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->integer('credits');
        $table->string('stripe_price_id');
        $table->decimal('price', 10, 2);
        $table->timestamps();
    });
}

// database/migrations/2025_01_24_create_credit_balances_table.php
public function up(): void
{
    Schema::create('credit_balances', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->integer('credits');
        $table->timestamp('expires_at')->nullable();
        $table->timestamps();
    });
}

// database/migrations/2025_01_24_create_credit_transactions_table.php
public function up(): void
{
    Schema::create('credit_transactions', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->integer('amount');
        $table->string('description');
        $table->string('type'); // purchase, usage, expiry, refund
        $table->json('metadata')->nullable();
        $table->timestamps();
    });
}

Now, let's create models for our credit system:

// app/Models/CreditPackage.php
class CreditPackage extends Model
{
    protected $fillable = ['name', 'credits', 'stripe_price_id', 'price'];
}

// app/Models/CreditBalance.php
class CreditBalance extends Model
{
    protected $fillable = ['user_id', 'credits', 'expires_at'];
    
    protected $casts = [
        'expires_at' => 'datetime',
    ];
}

// app/Models/CreditTransaction.php
class CreditTransaction extends Model
{
    protected $fillable = [
        'user_id', 'amount', 'description', 
        'type', 'metadata'
    ];
    
    protected $casts = [
        'metadata' => 'array',
    ];
}

Next, let's create a service to handle credit operations:

// app/Services/CreditService.php
class CreditService
{
    public function purchase(User $user, CreditPackage $package): void
    {
        DB::transaction(function () use ($user, $package) {
            // Create balance record
            CreditBalance::create([
                'user_id' => $user->id,
                'credits' => $package->credits,
                'expires_at' => null,
            ]);
            
            // Record transaction
            CreditTransaction::create([
                'user_id' => $user->id,
                'amount' => $package->credits,
                'description' => "Purchased {$package->name}",
                'type' => 'purchase',
                'metadata' => ['package_id' => $package->id],
            ]);
        });
    }
    
    public function deduct(User $user, int $credits, string $reason): bool
    {
        return DB::transaction(function () use ($user, $credits, $reason) {
            $balance = $this->getCurrentBalance($user);
            
            if ($balance < $credits) {
                return false;
            }
            
            // Deduct from the oldest balance first (FIFO)
            $balances = CreditBalance::where('user_id', $user->id)
                ->where('credits', '>', 0)
                ->orderBy('created_at')
                ->get();
                
            $remaining = $credits;
            
            foreach ($balances as $balance) {
                $deduction = min($remaining, $balance->credits);
                $balance->credits -= $deduction;
                $balance->save();
                
                $remaining -= $deduction;
                
                if ($remaining === 0) {
                    break;
                }
            }
            
            // Record transaction
            CreditTransaction::create([
                'user_id' => $user->id,
                'amount' => -$credits,
                'description' => $reason,
                'type' => 'usage',
            ]);
            
            return true;
        });
    }
    
    public function getCurrentBalance(User $user): int
    {
        return CreditBalance::where('user_id', $user->id)
            ->where(function ($query) {
                $query->whereNull('expires_at')
                    ->orWhere('expires_at', '>', now());
            })
            ->sum('credits');
    }
}

Sample Usage

Let's look at how to integrate this credit system into your application. First, we'll set up the purchase flow using Laravel Cashier, then show how to use credits in your API.

💡
This part assumes you have already set up Laravel Cashier (Stripe) in your project. If you haven't already, please check out the "Getting Started with Stripe in Laravel" article.

Here's a controller handling credit purchases:

// app/Http/Controllers/CreditPurchaseController.php
class CreditPurchaseController extends Controller
{
    public function __construct(
        private CreditService $creditService
    ) {}

    public function checkout(CreditPackage $package)
    {
        return auth()->user()
            ->checkout([$package->stripe_price_id], [
                'success_url' => route('credits.success'),
                'cancel_url' => route('credits.cancel'),
            ]);
    }

    public function success(Request $request)
    {
        // Stripe webhook will handle the actual credit assignment
        return redirect()
            ->route('credits.index')
            ->with('success', 'Your purchase was successful!');
    }
}

Set up the webhook handler for successful payments:

// app/Listeners/StripeEventListener.php
class StripeEventListener
{
    public function handle(WebhookReceived $event): void
    {
        $payload = $event->payload;

        if ($payload['type'] !== 'checkout.session.completed') {
            return;
        }

        $session = $payload['data']['object'];
        $user = User::where('stripe_id', $session['customer'])->firstOrFail();
        $sessionItems = $user->stripe()->checkout->sessions->allLineItems($session['id']);
        $creditService = app(CreditService::class);

        foreach ($sessionItems->data as $lineItem) {
            $package = CreditPackage::where('stripe_price_id', $lineItem->price->id)
                ->firstOrFail();

            $creditService->purchase($user, $package);
        }
    }
}

Finally, let's implement credit usage in an API middleware:

// app/Http/Middleware/DeductCredits.php
class DeductCredits
{
    public function __construct(
        private CreditService $creditService
    ) {}

    public function handle(Request $request, Closure $next, int $credits = 1)
    {
        $user = $request->user();
        
        if (!$this->creditService->deduct($user, $credits, 'API call: ' . $request->path())) {
            return response()->json([
                'error' => 'Insufficient credits',
                'current_balance' => $this->creditService->getCurrentBalance($user)
            ], 402);
        }

        return $next($request);
    }
}

Don't forget to register the middleware alias. For example, in Laravel 11 you can do this inside bootstrap/app.php file, like so:

    // ...

    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'credits' => App\Http\Middleware\DeductCredits::class,
        ]);
    })
    
    // ...

Use the middleware in your routes:

// routes/api.php
Route::middleware(['auth:sanctum', 'credits:1'])->group(function () {
    Route::post('/process-image', ProcessImageController::class);
});

// Use different credit amounts for different endpoints
Route::post('/bulk-process', BulkProcessController::class)
    ->middleware(['auth:sanctum', 'credits:10']);

The above implementation will consume 1 or 10 credits respectively, and in case the user runs out of available credits, will return HTTP 402 (Payment Required) response. This is, of course, up to you to define whether negative credit balances should be allowed.

Best Practices and Gotchas

When implementing this system, keep these points in mind:

  1. Race Conditions
    Our implementation uses database transactions to prevent double-spending of credits, but you might need additional optimizations for high-traffic APIs.
  2. Failed Payments
    Always verify the payment was successful before crediting the account. Use Stripe's webhooks rather than success redirects for reliability.
  3. Monitoring and Alerting
    Set up alerts for when users' balances run low and implement a notification system to prevent service interruptions.
  4. Database Optimization
    For high-volume APIs, consider periodically consolidating credit balances and archiving old transactions to maintain performance.

By following this implementation, you'll have a solid foundation for a credit-based billing system that can scale with your business. The code is structured to be easily extensible for additional features like volume discounts or time-based expiration rules.

Summary

Building a credit-based billing system requires careful consideration of both business and technical aspects. While this guide provides a solid foundation for implementing credit sales in Laravel, it's worth noting that managing credits at scale comes with additional complexity around handling edge cases, performance optimization, and business reporting.

For teams looking to move faster, platforms like Spike already solve many of these challenges. Spike specializes in credit-based billing and provides ready-made solutions for credit management, reporting, and scale – letting you focus on building your core product instead of billing infrastructure.

Whether you choose to build your own system or use a specialized platform, credit-based pricing can provide significant benefits for both your business and your customers. It offers improved cash flow through prepayments, reduces transaction costs, and gives customers the flexibility they need to manage their API usage effectively.

The key to success lies in making informed decisions about your pricing structure early on and implementing a robust technical foundation that can grow with your business.


Want to skip the complexity of building and maintaining your own billing infrastructure?

Spike Billing provides a complete, self-hosted billing solution that integrates seamlessly with both Stripe and Paddle. Your customers get a fully-featured portal to manage their subscriptions, one-time purchases, and billing details, while you maintain full control over the experience.

Get started in minutes with our Laravel package, or explore our extensive customization options to match your specific business needs. Visit documentation to get started, or have a look at a live demo.