Guides

One-time Products & Partial Payments

Sell bundles and courses that are paid once and never renew, and let customers pay with a deposit or in instalments.

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.
Use a plan only when the customer should be billed again every period. A "3-month bundle" that is paid once is a one-time product, even if the access it gives lasts three months. Your app decides how long access lasts. PimaBill records the sale.

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 unitAmount itself, for example a discounted price; description then overrides the display name.
  • If the add-on has no price in the invoice currency, the request fails with 400. Add the price, or pass unitAmount.
  • 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

WebhookWhen
invoice.partially_paidA payment was counted but something is still due. The payload has amount (this payment), amountPaid, amountDue, total and currency.
invoice.paidThe payments now cover the invoice.
payment.succeededEach 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.paid only.
  • Access on the deposit: grant on the first invoice.partially_paid, and remind or suspend if invoice.paid doesn't arrive by the date you set.
  • Pro-rata access: compare amountPaid to total and 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.

Copyright © 2026