# Credit Based Billing

> Guide for implementing credit-based billing with Dodo Payments - credit entitlements, balances, ledger management, rollover, overage, and meter-based deduction.

- **Type:** Skill
- **Install:** `agentstack add skill-dodopayments-skills-credit-based-billing`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [dodopayments](https://agentstack.voostack.com/s/dodopayments)
- **Installs:** 0
- **Category:** [Finance & Payments](https://agentstack.voostack.com/c/finance-and-payments)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [dodopayments](https://github.com/dodopayments)
- **Source:** https://github.com/dodopayments/skills/tree/main/dodo-payments/credit-based-billing
- **Website:** https://docs.dodopayments.com/developer-resources/agent-skills

## Install

```sh
agentstack add skill-dodopayments-skills-credit-based-billing
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Dodo Payments Credit-Based Billing

**Reference: [docs.dodopayments.com/features/credit-based-billing](https://docs.dodopayments.com/features/credit-based-billing)**

Grant customers a balance of credits (API calls, tokens, compute units, or any custom metric) and deduct from that balance as they consume your service.

---

## Overview

Credit-based billing lets you:
- **Issue credits** with subscriptions, one-time purchases, or via API
- **Deduct automatically** via usage meters or manually via API
- **Configure rollover** to carry unused credits forward
- **Handle overage** when credits run out mid-cycle
- **Set expiration** rules per credit entitlement
- **Track everything** via a full audit ledger

Credits work across all product types: subscriptions, one-time purchases, and usage-based billing.

---

## Core Concepts

### Credit Types

| Type | Description | Best For |
|------|-------------|----------|
| **Custom Unit** | Your own metric (tokens, API calls, compute hours) with configurable precision (0–3 decimals) | API calls, AI tokens, compute hours, messages |
| **Fiat Credits** | Real currency value (USD, EUR, etc.) that depletes as customers use your service | Prepaid balances, promotional credits, compensation |

### Credit Lifecycle

1. **Credits Issued** — Granted on purchase (subscription cycle or one-time) or via API
2. **Credits Consumed** — Deducted via meter events or manual API calls
3. **Credits Expire or Roll Over** — At cycle end, unused credits expire or carry forward
4. **Overage Handling** — If balance hits zero, overage is forgiven, billed, or carried as deficit

### Grant Sources

| Source | Description |
|--------|-------------|
| **Subscription** | Credits issued each billing cycle |
| **One-Time** | Credits issued with a one-time payment |
| **API** | Credits granted manually via API or dashboard |
| **Rollover** | Credits carried over from a previous billing cycle |

---

## Quick Start

### 1. Create a Credit Entitlement

```typescript
import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
});

const credit = await client.creditEntitlements.create({
  name: 'API Credits',
  credit_type: 'custom_unit',
  unit_name: 'API Calls',
  precision: 0,
  expiry_duration: 30, // days
  rollover_enabled: false,
  allow_overage: false,
});
```

### 2. Attach Credits to a Product

In Dashboard → Products → Create/Edit Product → Entitlements → Attach Credits:
- Select the credit entitlement
- Set credits issued per billing cycle (subscriptions) or total (one-time)
- Configure trial credits, proration, low balance threshold

### 3. Create Checkout with Credit Product

```typescript
const session = await client.checkoutSessions.create({
  product_cart: [
    {
      product_id: 'prod_ai_pro_plan', // Product with credits attached
      quantity: 1,
    }
  ],
  customer: { email: 'customer@example.com' },
  return_url: 'https://yourapp.com/success',
});

// Redirect to session.checkout_url
```

### 4. Deduct Credits via Usage Events

```typescript
// Meter linked to credit entitlement deducts automatically
await client.usageEvents.ingest({
  events: [{
    event_id: `gen_${Date.now()}_${crypto.randomUUID()}`,
    customer_id: 'cus_abc123',
    event_name: 'ai.generation',
    timestamp: new Date().toISOString(),
    metadata: { model: 'gpt-4', tokens: '1500' }
  }]
});
```

### 5. Check Balance

```typescript
const balance = await client.creditEntitlements.balances.get(
  'cent_credit_id',
  'cus_abc123'
);

console.log(`Available: ${balance.available_balance}`);
console.log(`Overage: ${balance.overage_balance}`);
```

---

## API Reference

### Credit Entitlement CRUD

| Operation | Method | Endpoint |
|-----------|--------|----------|
| Create | `POST` | `/credit-entitlements` |
| List | `GET` | `/credit-entitlements` |
| Get | `GET` | `/credit-entitlements/{id}` |
| Update | `PATCH` | `/credit-entitlements/{id}` |
| Delete | `DELETE` | `/credit-entitlements/{id}` |
| Undelete | `POST` | `/credit-entitlements/{id}/undelete` |

### Balance & Ledger Operations

| Operation | Method | Endpoint |
|-----------|--------|----------|
| List All Balances | `GET` | `/credit-entitlements/{id}/balances` |
| Get Customer Balance | `GET` | `/credit-entitlements/{id}/balances/{customer_id}` |
| Create Ledger Entry | `POST` | `/credit-entitlements/{id}/balances/{customer_id}/ledger-entries` |
| List Customer Ledger | `GET` | `/credit-entitlements/{id}/balances/{customer_id}/ledger` |
| List Customer Grants | `GET` | `/credit-entitlements/{id}/balances/{customer_id}/grants` |

---

## Implementation Examples

### TypeScript/Node.js

#### Create Credit Entitlement

```typescript
import DodoPayments from 'dodopayments';

const client = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
});

// Custom unit credit (AI tokens)
const tokenCredit = await client.creditEntitlements.create({
  name: 'AI Tokens',
  credit_type: 'custom_unit',
  unit_name: 'tokens',
  precision: 0,
  expiry_duration: 30,
  rollover_enabled: true,
  max_rollover_percentage: 25,
  rollover_timeframe: 'month',
  max_rollover_count: 3,
  allow_overage: true,
  overage_limit: 50000,
  price_per_unit: 0.001,
  overage_behavior: 'bill_overage_at_billing',
});

// Fiat credit (USD balance)
const usdCredit = await client.creditEntitlements.create({
  name: 'Platform Credits',
  credit_type: 'fiat',
  unit_currency: 'USD',
  expiry_duration: 90,
  rollover_enabled: false,
  allow_overage: false,
});
```

#### Manual Credit/Debit via Ledger Entry

```typescript
// Grant credits manually (e.g., promotional bonus)
await client.creditEntitlements.balances.createLedgerEntry(
  'cent_credit_id',
  'cus_abc123',
  {
    type: 'credit',
    amount: '500',
    description: 'Welcome bonus - 500 free API credits',
    idempotency_key: `welcome_bonus_${customerId}`,
  }
);

// Debit credits manually (e.g., service compensation deduction)
await client.creditEntitlements.balances.createLedgerEntry(
  'cent_credit_id',
  'cus_abc123',
  {
    type: 'debit',
    amount: '100',
    description: 'Manual deduction for premium support',
    idempotency_key: `support_deduction_${Date.now()}`,
  }
);
```

#### Query Customer Balance and Ledger

```typescript
// Get current balance
const balance = await client.creditEntitlements.balances.get(
  'cent_credit_id',
  'cus_abc123'
);
console.log(`Balance: ${balance.available_balance}`);

// List all balances for a credit entitlement
const allBalances = await client.creditEntitlements.balances.list(
  'cent_credit_id'
);

// Get full transaction history
const ledger = await client.creditEntitlements.balances.listLedger(
  'cent_credit_id',
  'cus_abc123'
);

for (const entry of ledger.items) {
  console.log(`${entry.type}: ${entry.amount} | Balance: ${entry.balance_after}`);
}

// List credit grants
const grants = await client.creditEntitlements.balances.listGrants(
  'cent_credit_id',
  'cus_abc123'
);
```

#### Update Credit Entitlement Settings

```typescript
await client.creditEntitlements.update('cent_credit_id', {
  rollover_enabled: true,
  max_rollover_percentage: 50,
  allow_overage: true,
  overage_limit: 10000,
  price_per_unit: 0.002,
  overage_behavior: 'bill_overage_at_billing',
});
```

### Python

```python
from dodopayments import DodoPayments
import os
import uuid
from datetime import datetime

client = DodoPayments(bearer_token=os.environ["DODO_PAYMENTS_API_KEY"])

# Create credit entitlement
credit = client.credit_entitlements.create(
    name="AI Tokens",
    credit_type="custom_unit",
    unit_name="tokens",
    precision=0,
    expiry_duration=30,
    rollover_enabled=True,
    max_rollover_percentage=25,
    allow_overage=True,
    overage_limit=50000,
    price_per_unit=0.001,
    overage_behavior="bill_overage_at_billing",
)

# Grant credits manually
client.credit_entitlements.balances.create_ledger_entry(
    credit_entitlement_id="cent_credit_id",
    customer_id="cus_abc123",
    type="credit",
    amount="500",
    description="Promotional bonus",
    idempotency_key=f"promo_{uuid.uuid4()}",
)

# Check balance
balance = client.credit_entitlements.balances.get(
    credit_entitlement_id="cent_credit_id",
    customer_id="cus_abc123",
)
print(f"Available: {balance.available_balance}")

# Send usage events that deduct credits
client.usage_events.ingest(events=[{
    "event_id": f"api_{datetime.now().timestamp()}_{uuid.uuid4()}",
    "customer_id": "cus_abc123",
    "event_name": "ai.tokens",
    "timestamp": datetime.now().isoformat(),
    "metadata": {"tokens": "1500", "model": "gpt-4"}
}])
```

### Go

```go
package main

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/dodopayments/dodopayments-go"
    "github.com/google/uuid"
)

func main() {
    client := dodopayments.NewClient(
        option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
    )

    ctx := context.Background()

    // Create credit entitlement
    credit, err := client.CreditEntitlements.Create(ctx, &dodopayments.CreditEntitlementCreateParams{
        Name:       "AI Tokens",
        CreditType: "custom_unit",
        UnitName:   "tokens",
        Precision:  0,
    })
    if err != nil {
        panic(err)
    }

    // Get customer balance
    balance, err := client.CreditEntitlements.Balances.Get(ctx, credit.ID, "cus_abc123")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Balance: %s\n", balance.AvailableBalance)

    // Send usage events
    _, err = client.UsageEvents.Ingest(ctx, &dodopayments.UsageEventIngestParams{
        Events: []dodopayments.UsageEvent{{
            EventID:    fmt.Sprintf("api_%d_%s", time.Now().Unix(), uuid.New().String()),
            CustomerID: "cus_abc123",
            EventName:  "ai.tokens",
            Timestamp:  time.Now().Format(time.RFC3339),
            Metadata: map[string]string{
                "tokens": "1500",
                "model":  "gpt-4",
            },
        }},
    })
    if err != nil {
        panic(err)
    }
}
```

---

## Credit Settings

### Rollover

Carry unused credits forward to the next billing cycle:

| Setting | Description |
|---------|-------------|
| **Rollover Enabled** | Toggle to allow unused credits to carry forward |
| **Max Rollover Percentage** | Limit how much carries over (0–100%) |
| **Rollover Timeframe** | How long rolled-over credits remain valid (day, week, month, year) |
| **Max Rollover Count** | Maximum consecutive rollovers before credits are forfeited |

**Example**: 200 unused credits at cycle end, 75% rollover → 150 credits carry forward, 50 forfeited.

### Overage

Controls what happens when a customer's balance reaches zero mid-cycle:

| Setting | Description |
|---------|-------------|
| **Allow Overage** | Let customers continue past zero balance |
| **Overage Limit** | Max credits consumable beyond balance |
| **Price Per Unit** | Cost per additional credit (with currency) |
| **Overage Behavior** | How overage is handled at cycle end |

**Overage Behaviors:**

| Behavior | Description |
|----------|-------------|
| **Forgive overage at reset** | Overage tracked but not billed (default) |
| **Bill overage at billing** | Overage charged on next invoice |
| **Carry over deficit** | Negative balance carries into next cycle |
| **Carry over deficit (auto-repay)** | Deficit auto-repaid from new credits next cycle |

### Expiration

| Setting | Description |
|---------|-------------|
| **Credit Expiry** | Duration after issuance: 7, 30, 60, 90, custom days, or never |
| **Trial Credits Expire After Trial** | Whether trial-specific credits expire when trial ends |

---

## Webhook Events

Credit-based billing fires these webhook events:

| Event | Description |
|-------|-------------|
| `credit.added` | Credits granted to a customer |
| `credit.deducted` | Credits consumed through usage or manual debit |
| `credit.expired` | Unused credits expired |
| `credit.rolled_over` | Credits carried forward to a new grant |
| `credit.rollover_forfeited` | Credits forfeited at max rollover count |
| `credit.overage_charged` | Overage charges applied |
| `credit.manual_adjustment` | Manual credit/debit adjustment made |
| `credit.balance_low` | Balance dropped below configured threshold |

### Webhook Handler Example

```typescript
// app/api/webhooks/dodo/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const event = await req.json();

  switch (event.type) {
    case 'credit.added':
      await handleCreditAdded(event.data);
      break;
    case 'credit.deducted':
      await handleCreditDeducted(event.data);
      break;
    case 'credit.balance_low':
      await handleBalanceLow(event.data);
      break;
    case 'credit.expired':
      await handleCreditExpired(event.data);
      break;
    case 'credit.overage_charged':
      await handleOverageCharged(event.data);
      break;
  }

  return NextResponse.json({ received: true });
}

async function handleCreditAdded(data: any) {
  const { customer_id, credit_entitlement_id, amount, balance_after } = data;
  
  // Update internal records
  await prisma.creditBalance.upsert({
    where: { customerId_creditId: { customerId: customer_id, creditId: credit_entitlement_id } },
    create: { customerId: customer_id, creditId: credit_entitlement_id, balance: balance_after },
    update: { balance: balance_after },
  });
}

async function handleCreditDeducted(data: any) {
  const { customer_id, credit_entitlement_id, amount, balance_after } = data;

  await prisma.creditBalance.update({
    where: { customerId_creditId: { customerId: customer_id, creditId: credit_entitlement_id } },
    data: { balance: balance_after },
  });
}

async function handleBalanceLow(data: any) {
  const {
    customer_id,
    credit_entitlement_name,
    available_balance,
    threshold_percent,
  } = data;

  // Notify the customer
  await sendEmail(customer_id, {
    subject: `Your ${credit_entitlement_name} balance is running low`,
    body: `You have ${available_balance} credits remaining (${threshold_percent}% threshold reached). Consider upgrading your plan or purchasing additional credits.`,
  });
}

async function handleCreditExpired(data: any) {
  const { customer_id, credit_entitlement_id, amount, balance_after } = data;

  await prisma.creditBalance.update({
    where: { customerId_creditId: { customerId: customer_id, creditId: credit_entitlement_id } },
    data: { balance: balance_after },
  });

  // Optionally notify customer
  await sendCreditExpiryNotification(customer_id, amount);
}

async function handleOverageCharged(data: any) {
  const { customer_id, credit_entitlement_id, amount, overage_after } = data;

  // Track overage for billing
  await prisma.overageRecord.create({
    data: {
      customerId: customer_id,
      creditId: credit_entitlement_id,
      amount,
      overageBalance: overage_after,
    },
  });
}
```

### Balance Low Payload

The `credit.balance_low` event has a distinct payload:

```json
{
  "business_id": "bus_xxxxx",
  "type": "credit.balance_low",
  "timestamp": "2025-08-04T06:15:00.000000Z",
  "data": {
    "payload_type": "CreditBalanceLow",
    "customer_id": "cus_xxxxx",
    "subscription_id": "sub_xxxxx",
    "credit_entitlement_id": "cent_xxxxx",
    "credit_entitlement_name": "API Credits",
    "available_balance": "15",
    "subscription_credits_amount": "100",
    "threshold_percent": 20,
    "threshold_amount": "20"
  }
}
```

---

## Usage Billing with Credits

When credits are linked to usage meters, meter events automatically deduct credits. A background worker processes events every minute, converts meter units to credits using your configured rate, and deducts using FIFO ordering (oldest grants first).

### How It Works

1. **Your app sends usage events** — Each event includes customer ID, event name, and metadata
2. **Meters aggregate events** — U

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [dodopayments](https://github.com/dodopayments)
- **Source:** [dodopayments/skills](https://github.com/dodopayments/skills)
- **License:** MIT
- **Homepage:** https://docs.dodopayments.com/developer-resources/agent-skills

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-dodopayments-skills-credit-based-billing
- Seller: https://agentstack.voostack.com/s/dodopayments
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
