One-time Products & Partial Payments
Not everything is a subscription. A course, a certification bundle, an installation or an event ticket is bought once. Some buyers also can't pay it all at once, so they pay a deposit first, or pay in instalments.
PimaBill handles both:
- One-time products are add-ons: a catalogue item with a price in each currency. You sell one on an invoice, and nothing renews.
- Partial payments: any invoice can be paid in parts. It stays open until the payments cover it, and then it is marked paid.
1. Put the product in the catalogue
Create an add-on once, with a price in each currency you sell in. The code is how you refer to it everywhere else, so pick something stable. It is the same in sandbox and live, unlike IDs.
curl -X POST https://api.pimabill.com/api/add-ons \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Cloud Certification Bundle (3 months)",
"code": "cert-bundle-3m",
"invoiceDisplayName": "Cloud Certification Bundle — 3 months",
"prices": [
{ "currency": "UGX", "amount": 450000 },
{ "currency": "KES", "amount": 16000 },
{ "currency": "USD", "amount": 120 }
]
}'
GET /api/add-ons lists the catalogue. PATCH /api/add-ons/{id} changes the name or prices; invoices that already exist keep the price they were sold at.
2. Sell it
Create an invoice with an item that names the add-on by addOnCode. Its price in the invoice currency and its display name are filled in for you. Add checkout to get a payment link in the same call:
curl -X POST https://api.pimabill.com/api/invoices \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"customerId": "cus_123",
"currency": "UGX",
"items": [{ "addOnCode": "cert-bundle-3m", "quantity": 1 }],
"dueDate": "2026-11-09",
"checkout": { "callbackUrl": "https://learn.example.com/paid" }
}'
Send the customer checkout.checkoutUrl. When the payment is confirmed you receive invoice.paid, and that is when you grant access.
- An item can still set
unitAmountitself, for example a discounted price;descriptionthen overrides the display name. - If the add-on has no price in the invoice currency, the request fails with
400. Add the price, or passunitAmount. - To sell several products together, add several items to the same invoice.
3. Let them pay in parts
Every invoice has an amountPaid, next to its amount. A payment of less than what is due is counted, and the invoice stays open (PENDING) until the payments add up to the full amount. Then it becomes PAID.
A deposit at checkout
Pass checkout.amount to charge part of the invoice now:
"checkout": { "callbackUrl": "https://learn.example.com/paid", "amount": 150000 }
The next instalment
Ask for a new payment link for the same invoice. Without amount, the link charges everything still due:
curl -X POST https://api.pimabill.com/api/invoices/inv_123/checkout \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "amount": 150000, "callbackUrl": "https://learn.example.com/paid" }'
The amount must be more than zero and at most what is still due.
Cash, bank transfer or mobile money paid outside PimaBill
Record it with mark-paid and an amount. Without amount, it records everything still due:
curl -X POST https://api.pimabill.com/api/invoices/inv_123/mark-paid \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "paymentMethod": "bank_transfer", "amount": 150000 }'
In the dashboard, Mark paid has an Amount received field. Anything less than what is due records a part payment, and the invoice shows as partially paid.
What you are told
| Webhook | When |
|---|---|
invoice.partially_paid | A payment was counted but something is still due. The payload has amount (this payment), amountPaid, amountDue, total and currency. |
invoice.paid | The payments now cover the invoice. |
payment.succeeded | Each payment, partial or not. |
Refunding a part payment takes it back off amountPaid, so the invoice is due that much more again. See Refund a payment.
Deciding when to give access
Partial payments are your policy, so choose what each payment unlocks:
- Access on full payment (the simplest): grant on
invoice.paidonly. - Access on the deposit: grant on the first
invoice.partially_paid, and remind or suspend ifinvoice.paiddoesn't arrive by the date you set. - Pro-rata access: compare
amountPaidtototaland unlock that share, such as one module per instalment.
To check the state at any time, GET /api/invoices/{id} returns status, amount and amountPaid.
Sandbox first
Do the whole flow with a sk_test_ key against https://sandbox.pimabill.com/api first. The add-on code is the same in both environments, but IDs (customers, invoices) are not, so store codes in your app rather than IDs. See Sandbox.