Invoices & Payments
PimaBill automatically generates invoices for subscriptions and processes payments through your configured providers.
Invoice Lifecycle
DRAFT → PENDING → PAID
→ FAILED → PAID (after retry)
→ CANCELED (voided)
| Status | Meaning |
|---|---|
DRAFT | Being prepared; can still be changed. Finalize it to send it. |
PENDING | Sent and waiting for payment. If amountPaid is above zero, it has been partially paid. |
PAID | Payments cover the whole amount. paidAt is when that happened. |
FAILED | An automatic charge failed; it is retried, and it can still be paid by link or marked paid. |
CANCELED | Voided; it can no longer be paid. |
Every invoice has amount (the total) and amountPaid (what has been paid so far). What's still due is amount − amountPaid.
Paying in parts
An invoice can be paid in several payments: a deposit, then instalments. Each payment is added to amountPaid; the invoice stays PENDING until the payments cover it, then becomes PAID.
- Online:
POST /api/invoices/{id}/checkoutwith{ "amount": 50000 }charges that much. Withoutamount, a checkout charges everything still due, so a second link after a deposit asks only for the rest. - Offline (cash, bank transfer, mobile money to your own number):
POST /api/invoices/{id}/mark-paidwith{ "paymentMethod": "cash", "amount": 50000 }. Withoutamount, it records everything still due. In the dashboard, Mark paid has an Amount received field. - At creation:
checkout.amountonPOST /api/invoicestakes a deposit in the same call.
Each part payment sends invoice.partially_paid (with amountPaid, amountDue and total); the last one sends invoice.paid. See One-time products & partial payments for a full walkthrough.
List Invoices
curl "http://localhost:4000/api/invoices?status=PENDING&page=1&limit=20" \
-H "Authorization: Bearer sk_live_a1b2c3d4..."
import { PimaBillClient } from 'pimabill';
const client = new PimaBillClient({ token: 'sk_live_a1b2c3d4...' });
const invoices = await client.invoices.list({
status: 'PENDING',
page: 1,
limit: 20
});
for (const invoice of invoices.data) {
console.log(`${invoice.id}: ${invoice.currency} ${invoice.amount} - ${invoice.status}`);
}
from pimabill import PimaBillClient
client = PimaBillClient(token="sk_live_a1b2c3d4...")
invoices = client.invoices.list(status="PENDING", page=1, limit=20)
for invoice in invoices.data:
print(f"{invoice.id}: {invoice.currency} {invoice.amount} - {invoice.status}")
Create a Manual Invoice
curl -X POST http://localhost:4000/api/invoices \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_a1b2c3d4..." \
-d '{
"customerId": "cust_x1y2z3",
"dueDate": "2025-02-28",
"items": [
{"description": "Pro Plan - Monthly", "quantity": 1, "unitAmount": 180000}
]
}'
const invoice = await client.invoices.create({
customerId: 'cust_x1y2z3',
dueDate: '2025-02-28',
items: [
{ description: 'Pro Plan - Monthly', quantity: 1, unitAmount: 180000 }
]
});
from pimabill import InvoiceItemDto
invoice = client.invoices.create(
customer_id="cust_x1y2z3",
due_date="2025-02-28",
items=[InvoiceItemDto(description="Pro Plan - Monthly", quantity=1, unit_amount=180000)],
)
Finalize and Pay
# Finalize a draft invoice
curl -X POST http://localhost:4000/api/invoices/inv_abc123/finalize \
-H "Authorization: Bearer sk_live_a1b2c3d4..."
# Mark as manually paid (add "amount" to record only part of it)
curl -X POST http://localhost:4000/api/invoices/inv_abc123/mark-paid \
-H "Authorization: Bearer sk_live_a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{"paymentMethod": "bank_transfer"}'
# Void an invoice
curl -X POST http://localhost:4000/api/invoices/inv_abc123/void \
-H "Authorization: Bearer sk_live_a1b2c3d4..."
// Finalize a draft invoice
await client.invoices.finalize('inv_abc123');
// Mark as manually paid
await client.invoices.markPaid('inv_abc123');
// Void an invoice
await client.invoices.void('inv_abc123');
client.invoices.finalize("inv_abc123")
client.invoices.mark_paid("inv_abc123")
client.invoices.void("inv_abc123")
Download PDF
curl "http://localhost:4000/api/invoices/inv_abc123/pdf" \
-H "Authorization: Bearer sk_live_a1b2c3d4..." \
-o invoice.pdf
import { writeFile } from 'node:fs/promises';
const pdf = await client.invoices.getPdf('inv_abc123');
await writeFile('invoice.pdf', Buffer.from(await pdf.arrayBuffer()));
with open("invoice.pdf", "wb") as f:
for chunk in client.invoices.get_pdf("inv_abc123"):
f.write(chunk)
Checkout
Generate a hosted checkout URL for an invoice so customers can pay online. It charges what is still due, or amount if you pass one (see Paying in parts). Pass providerName to choose a provider; otherwise your highest-priority active provider is used, and managed payments when none of yours can take it:
curl -X POST http://localhost:4000/api/invoices/inv_abc123/checkout \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_a1b2c3d4..." \
-d '{}'
const checkout = await client.invoices.createCheckout('inv_abc123');
console.log(checkout.checkoutUrl);
checkout = client.invoices.create_checkout("inv_abc123")
print(checkout.checkout_url)
Payments
Payments are created automatically when invoices are processed. You can list and refund them.
List Payments
curl "http://localhost:4000/api/payments?page=1&limit=20" \
-H "Authorization: Bearer sk_live_a1b2c3d4..."
const payments = await client.payments.list({
page: 1,
limit: 20
});
payments = client.payments.list(page=1, limit=20)
Refund a Payment
curl -X POST http://localhost:4000/api/payments/pay_abc123/refund \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_live_a1b2c3d4..." \
-d '{
"amount": 50000,
"reason": "Customer overcharged"
}'
const refund = await client.payments.refund('pay_abc123', {
amount: 50000, // Partial refund in UGX
reason: 'Customer overcharged'
});
refund = client.payments.refund(
"pay_abc123",
amount=50000,
reason="Customer overcharged"
)
The refund goes back through the provider that took the payment (pass "recordOnly": true if you already refunded it elsewhere), and a payment can be refunded once. What was refunded comes off the invoice's amountPaid:
- An open invoice (a deposit or instalment was refunded) is due that much more again.
- A paid invoice stays
PAID; the refund is recorded on the payment, andpayment.refundedis sent. - A duplicate payment (money that arrived after the invoice was already paid or voided, flagged by
payment.duplicate) never counted toward the invoice, so refunding it leaves the invoice as it is.
Payment Statuses
| Status | Description |
|---|---|
PENDING | Payment initiated but not yet processed |
PROCESSING | Payment is being processed by the provider |
SUCCEEDED | Payment completed successfully |
FAILED | Payment attempt failed |
REFUNDED | Payment has been refunded |