Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Credit Wallets

The credit wallet system supports prepaid top-up credits, automatic drawdown against invoices, and auto-recharge workflows. It’s backed by the TigerBeetle ledger for double-entry accounting.

Overview

Each customer has a prepaid credit wallet — a TigerBeetle account that holds a positive balance representing available credit. When an invoice is generated, the billing engine checks the wallet balance and applies available credits before charging the customer’s payment method.

Issue a Wallet Credit (Admin)

Credits are issued by Finance or Support via an admin-only endpoint. Every credit is:

  • Idempotent — safe to retry with the same idempotency_key
  • Audited — creates an audit log entry with actor, reason, and amount
  • Ledger-backed — debits the platform liability account and credits the customer wallet account in TigerBeetle
# [curl]
curl -X POST https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID/credits \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_nanos": "10000000000000",
    "wallet_account_id": "123456789",
    "platform_account_id": "999888777",
    "description": "Service credit — February outage compensation",
    "reason": "4-hour outage on 2026-02-28 per incident INC-4421",
    "actor_id": "support-agent-001",
    "actor_name": "Jane Smith",
    "idempotency_key": "credit-inc-4421-acme"
  }'
# [Python]
import requests

resp = requests.post(
    f"https://api.bill.sh/admin/v1/customers/{customer_id}/credits",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
    json={
        "amount_nanos": "10000000000000",          # $10.00
        "wallet_account_id": wallet_account_id,
        "platform_account_id": platform_account_id,
        "description": "Service credit — February outage compensation",
        "reason": "4-hour outage on 2026-02-28 per incident INC-4421",
        "actor_id": "support-agent-001",
        "actor_name": "Jane Smith",
        "idempotency_key": "credit-inc-4421-acme",
    },
)
result = resp.json()
print(f"Transfer: {result['transfer_id']}, Audit: {result['audit_entry_id']}")
credit_usd = 10_000_000_000_000 / 1e12
print(f"Issued ${credit_usd:.2f} credit to customer {customer_id}")
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/customers/${customerId}/credits`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${ADMIN_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      amount_nanos: "10000000000000",           // $10.00
      wallet_account_id: walletAccountId,
      platform_account_id: platformAccountId,
      description: "Service credit — February outage compensation",
      reason: "4-hour outage on 2026-02-28 per incident INC-4421",
      actor_id: "support-agent-001",
      actor_name: "Jane Smith",
      idempotency_key: "credit-inc-4421-acme",
    }),
  }
);
const result = await resp.json();
console.log(`Transfer: ${result.transfer_id}, Audit: ${result.audit_entry_id}`);
// [Go]
body, _ := json.Marshal(map[string]string{
    "amount_nanos":        "10000000000000",
    "wallet_account_id":   walletAccountID,
    "platform_account_id": platformAccountID,
    "description":         "Service credit — February outage compensation",
    "reason":              "4-hour outage on 2026-02-28 per incident INC-4421",
    "actor_id":            "support-agent-001",
    "actor_name":          "Jane Smith",
    "idempotency_key":     "credit-inc-4421-acme",
})
req, _ := http.NewRequest("POST",
    "https://api.bill.sh/admin/v1/customers/"+customerID+"/credits",
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
fmt.Printf("Transfer: %v, Audit: %v\n", result["transfer_id"], result["audit_entry_id"])

amount_nanos: 10000000000000 = $10.00 USD.

Response:

{
  "transfer_id": "01944b1f-0000-7000-8000-000000000020",
  "amount_nanos": "10000000000000",
  "audit_entry_id": "01944b1f-0000-7000-8000-000000000021"
}

View Credit History

# [curl]
curl https://api.bill.sh/admin/v1/customers/$CUSTOMER_ID/credits \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# [Python]
resp = requests.get(
    f"https://api.bill.sh/admin/v1/customers/{customer_id}/credits",
    headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
)
history = resp.json()
for entry in history:
    amount_usd = int(entry["amount_nanos"]) / 1e12
    print(f"{entry['created_at']} — ${amount_usd:.2f} by {entry['actor_name']}: {entry['reason']}")
// [Node.js]
const resp = await fetch(
  `https://api.bill.sh/admin/v1/customers/${customerId}/credits`,
  { headers: { "Authorization": `Bearer ${ADMIN_TOKEN}` } }
);
const history = await resp.json();
for (const entry of history) {
  const amountUsd = (BigInt(entry.amount_nanos) / BigInt(1e12)).toString();
  console.log(`${entry.created_at} — $${amountUsd} by ${entry.actor_name}`);
}
// [Go]
req, _ := http.NewRequest("GET",
    "https://api.bill.sh/admin/v1/customers/"+customerID+"/credits", nil)
req.Header.Set("Authorization", "Bearer "+adminToken)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var history []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&history)
for _, entry := range history {
    fmt.Printf("%v — %v by %v\n",
        entry["created_at"], entry["amount_nanos"], entry["actor_name"])
}

Returns audit log entries for all wallet credit actions on this customer.

Credit vs Credit Note

Wallet CreditCredit Note
WhenProactively, before invoiceAfter invoice is finalized
MechanismTigerBeetle balanceNegative invoice
Use caseGoodwill credit, prepaid top-upDispute resolution, partial refund
AccountingDebit platform liability / Credit walletNegative AR entry

Auto-Recharge

When the wallet balance drops below a threshold, the platform can automatically charge the customer’s payment method to top up. This is configured per-customer via the wallet service.