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 Credit | Credit Note | |
|---|---|---|
| When | Proactively, before invoice | After invoice is finalized |
| Mechanism | TigerBeetle balance | Negative invoice |
| Use case | Goodwill credit, prepaid top-up | Dispute resolution, partial refund |
| Accounting | Debit platform liability / Credit wallet | Negative 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.