Payments
Serront tracks payments the way Indonesian service businesses actually take them: manual bank transfer, with the buyer's receipt as proof — works on every plan, including Free. The optional Payment module (Starter+) adds real online checkout (QRIS, virtual account, e-wallet, card) on top.
Manual bank transfer (the default)
No money moves through Serront in this flow — the buyer transfers straight to your bank account. Serront's job is to show the buyer where to pay and to keep the payment status honest on both sides.
1. Instructions
Set up to 5 bank accounts (bank name, account number, holder) plus
free-form payment instructions at
/dashboard/storefront. They appear on the
buyer's order page (/o/<token>) only while payment isn't
confirmed yet — never on your public storefront page, and they
disappear once the payment is settled. If you've set a WhatsApp
number, a wa.me contact link shows alongside.
2. The buyer claims
After transferring, the buyer taps "I have transferred" on their
order page — paymentStatus moves unpaid → payment_claimed, and
the order surfaces in your dashboard as awaiting your check.
The buyer can also upload a transfer receipt (PNG, JPEG, WebP, or
GIF, up to 5 MB). You view it from the order detail (API:
GET /api/v1/orders/:id/proof).
3. You confirm
Check your bank (and the proof), then hit Confirm payment —
paymentStatus → payment_confirmed, the buyer gets a confirmation
email, and a serront.order.payment_confirmed.v1
webhook event fires. You can confirm
directly from unpaid too (cash in hand, transfer spotted first).
curl -X POST https://serront.com/api/v1/orders/ord_01jx…/confirm-payment \
-H "Authorization: Bearer sk_live_xxx"
Honestly: Serront never verifies the transfer — confirmation is your judgment call against your own bank statement. That's the nature of manual transfer; if you want machine-verified payment, that's the Payment module below.
Split payments (down payments & milestones)
Software-dev and other project work is rarely paid in one go — so any order's payment can be split into 2–8 installments ("Down payment", "Milestone 2", "Final payment", …). Works on every plan, for both manual transfer and online checkout.
From the order detail (Split payment in the payment panel), set a
label and amount per installment — presets for 50/50 and
30/40/30 included. The amounts must sum exactly to the quote;
a mismatch is rejected (409 SPLIT_SUM_MISMATCH). The same schedule
lives on the API:
curl -X PUT https://serront.com/api/v1/orders/ord_01jx…/payments \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"installments":[
{"label":"Down payment","amountIdr":600000},
{"label":"Final payment","amountIdr":400000}]}'
Once split:
- The buyer's order page shows the schedule with per-installment status and "N of M payments received" progress. Each unpaid installment has its own "I have transferred" claim, its own receipt upload (same 5 MB / image rules), and — with the Payment module on — its own Pay online button for that installment's amount.
- You confirm each installment separately
(
POST /api/v1/orders/:id/payments/:paymentId/confirm); an online payment confirms its installment automatically. Every confirm emails the buyer ("Payment Down payment (1 of 2) received — Rp 600.000") and fires aserront.order.installment_confirmed.v1webhook event; the final one also notes the order is fully paid and fires the usualserront.order.payment_confirmed.v1. - The order-level
paymentStatusis derived from the schedule: all installments confirmed →payment_confirmed; any claimed or confirmed →payment_claimed; otherwiseunpaid. No new statuses.
The rules, honestly stated:
- Confirmed money is immutable. A claimed or confirmed installment can't be edited or removed; the quote locks the moment any payment (installment or legacy) is confirmed. Unpaid installments stay editable — rebalance, relabel, add or remove them as the project shifts, as long as the sum still equals the quote.
- Splitting is blocked once the order is already paid, or after the buyer claimed the (unsplit) full payment — resolve that claim first.
- Removing a split entirely (back to one payment) only works while every installment is still unpaid.
- Orders without a split behave exactly as before — nothing changes until you split.
Quote breakdowns (pricing components)
A quote is often more honest as a sum of parts — frontend development, backend development, training, documentation. Any order's quote can carry an itemized breakdown: up to 20 components, each a label (≤140 chars) × quantity × unit price.
From the order detail (Quote breakdown next to the quote), or on the API:
curl -X PUT https://serront.com/api/v1/orders/ord_01jx…/quote-items \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"items":[
{"label":"Frontend development","quantity":1,"unitAmountIdr":6000000},
{"label":"Backend development","quantity":1,"unitAmountIdr":8000000},
{"label":"Training","quantity":2,"unitAmountIdr":500000},
{"label":"Documentation","quantity":1,"unitAmountIdr":1000000}]}'
The rules:
- Saving the breakdown sets the quote —
quotedPriceIdrbecomesΣ quantity×unitAmountIdrin the same transaction. While a breakdown exists, a direct quote edit must match its sum (409otherwise — edit the breakdown instead). - The same locks as the quote itself: blocked once any payment is
confirmed, and with a payment
split the new sum must
keep matching the schedule (
409 SPLIT_SUM_MISMATCH— adjust the schedule first). - Saving an empty list removes the breakdown; the quote stays as-is and the order goes back to a single-figure quote.
- The buyer sees the itemized table — label, qty × unit, line totals, grand total — on their order page and in the client portal. No breakdown → the single-figure quote renders exactly as before.
Invoices (Plugipay documents)
With the Payment module on, a quote can become a real invoice — a numbered, PDF-rendered document on your Plugipay workspace, with the breakdown as its line items.
- Issue — Issue invoice on the order detail, or
POST /api/v1/orders/:id/invoice. The buyer is linked as a Plugipay Customer (by email), the invoice is created open with memoOrder #N — <service>; lines are the quote breakdown, or one line for single-figure quotes. Re-POSTing while an un-voided invoice exists just returns it — issuing is idempotent. Aserront.order.invoice_issued.v1webhook event fires. - Renewal on quote change — if the quote (or the breakdown's
composition) changes while the invoice is still open, the old
document is voided and a fresh one is issued automatically from
the current state (
serront.order.invoice_renewed.v1). The document on the buyer's page is therefore never stale. A paid invoice never renews — the quote is locked by then anyway. - Paid on settlement — when the payment settles (manual confirm, online checkout, or the final installment), the invoice is marked paid on Plugipay so the document closes. This is best-effort: a Plugipay hiccup never blocks the settlement itself.
- Email it — Send to
on the order detail, or POST /api/v1/orders/:id/invoice/send. Plugipay emails the document; the last-sent time shows on the card. - The buyer's PDF link — the order page (
/o/<token>) and the client portal show "Invoice #N (PDF)". The bytes stream through Serront (GET /api/v1/public/orders/:token/invoice.pdf, portal:GET /api/v1/client/orders/:id/invoice.pdf) — the buyer never needs a Plugipay account; the order token / portal session is the credential. Your own copy lives atGET /api/v1/orders/:id/invoice.pdf.
Honestly: the invoice lives on Plugipay — numbering
(PLGP-…), statuses, the PDF rendering, and the email are all
Plugipay's. Serront stores only a display snapshot (number, status,
issued time) on the order. All invoice endpoints answer
409 PAYMENT_MODULE_DISABLED while the module is off; orders that
are declined/canceled or have no quote yet can't be invoiced
(409).
The Payment module — online checkout
Available on Starter and up (plans). Enable it at /dashboard/settings/modules — the first enable provisions a payment workspace for you on Plugipay (the Forjio family's payment service); no Plugipay sign-up needed.
Once on:
- The buyer's order page grows a "Pay online" button next to your transfer instructions. It opens a Plugipay-hosted checkout for the order's quote — QRIS, virtual account, e-wallet, or card.
- A completed payment settles the order to
payment_confirmedautomatically (webhook-driven and idempotent — a retried notification never double-settles). The buyer gets the same confirmation email a manual confirm sends. - A 0.3% module fee applies to amounts paid online. Manual transfers stay free — both paths remain available side by side, the buyer picks.
Orders that are declined or canceled, already confirmed, or have no
quote yet can't be paid online (409).
Your money: balance and payouts
With online payment, funds collect in your Plugipay-held balance — see it all on the Payments page (/dashboard/payments):
- Balance — available = ledger balance minus in-flight payouts.
- Transactions — your ledger entries.
- Payouts — request a payout to your bank account; payouts move
pending → in_transit → paid(you can cancel whilepending). - Bank account — the default payout destination.
The same surfaces exist on the API: GET /api/v1/ledger/balance,
GET /api/v1/ledger/entries, GET|POST /api/v1/payouts,
GET|PATCH /api/v1/payouts/bank-account. All of them answer
409 PAYMENT_MODULE_DISABLED while the module is off.
curl -H "Authorization: Bearer sk_live_xxx" \
https://serront.com/api/v1/payouts/balance
Honestly: payouts are operator-processed in v1 — a requested payout is reviewed and transferred manually, not instant. Disabling the module just flips it off; your Plugipay workspace, balance, and history stay intact and come back on re-enable.
See also
- Orders — the payment track in the order lifecycle.
- Billing & plans — module availability per tier.
- Webhooks —
serront.order.payment_confirmed.v1.