How a payment settles
A payment is asynchronous: accepting it and settling it are two separate steps. This page walks through exactly what Billy does with your POST /billers/{id}/payments request — the checks it runs, what your float does, and how you find out the result — so nothing about the outcome is a surprise.
The one thing to internalise
A 201 Created means “accepted, and your money is ring-fenced” — not “the bill is paid.” The payment comes back with status processing. The real posting to the biller happens a moment later, and that’s what flips the payment to succeeded or failed. You learn the terminal result from a webhook (or by polling the payment).
1 · The gate — before any money moves
Several checks stand between your request and any charge. If any fails, no payment is created and nothing is reserved:
- Authentication. Your API key is resolved to your account. A missing or revoked key, or a suspended/closed account, is rejected with
401. - Rate limit. Requests are throttled per account; bursts get
429. - Biller availability. A biller you’re not permitted to pay (a restricted biller you haven’t been approved for) returns
404— indistinguishable from one that doesn’t exist. - Validation. The amount must be positive and within the biller’s min/max, the fields must match the biller’s schema, and an
Idempotency-Keyheader is required. Anything off is a422with details.
2 · Billy reprices — your total is never trusted
Even though you may have quoted first, Billy recomputes the fees server-side from the biller and your pricing, and snapshots them onto the payment. That snapshot is frozen for the life of the payment — a later fee change can never rewrite what this one cost. It’s also why a quote and the payment that follows it can never disagree: the same calculator produces both.
3 · The ring-fence — your float moves, but isn’t spent
For a wallet-funded payment, Billy moves the full customer total out of your available balance into a held balance, inside a single locked transaction:
available − customer_total
held + customer_total
The money is now parked in your own held balance — earmarked for this bill but not yet spent. If you can’t cover the total (available balance vs. your credit limit), the whole thing rolls back: you get a 422 and no payment is created. The lock is what makes concurrent payments safe — two requests can’t both spend the same float.
At this point the payment is processing, a payment.processing webhook fires, and your 201 is returned.
4 · Posting — asynchronous, and reversible until it isn’t
Billy then posts the bill to the biller’s provider on a background queue. One of two things happens:
- Success. The held amount settles the payment (the bill and its fees leave your float). The payment becomes
succeeded, with the provider’s reference recorded, and apayment.succeededwebhook fires. - Failure. The hold is released straight back to your available balance. The payment becomes
failedwith a reason, and apayment.failedwebhook fires. A failed payment costs you nothing — the money never left your wallet; it only sat in your held balance and came back.
Your float, in three moves:
accept available → held (ring-fenced, reversible)
success held → settled (money leaves your float)
failure held → available (money returns to you)
Statuses you’ll see
pending— created, being accepted.processing— accepted and ring-fenced; posting in flight.succeeded— posted and settled. Terminal.failed— posting failed; funds returned. Terminal.awaiting_payment— a “Billy collects” payment waiting on the end-customer’s checkout (see below).
How you learn the outcome
Don’t block on the API response for the result — it’s processing. Instead:
- Listen for webhooks —
payment.succeeded/payment.failed— the recommended path. - Or poll
GET /payments/{id}until the status is terminal.
Retries are safe
If your request times out and you retry with the same Idempotency-Key, Billy returns the original payment instead of creating a second one. You will never double-charge by retrying. See Errors & idempotency.
The “Billy collects” variant
If you send funding_source: "collect", Billy doesn’t reserve your float. It returns a hosted checkout_url for the end-customer to pay, and the payment stays awaiting_payment until they do. Once the checkout completes, the same posting step runs — settled from the collected funds rather than your float. A posting failure there refunds the customer’s charge automatically.
Collect carries a gateway_fee — the payment gateway’s charge — surcharged to the payer, so the customer pays a little more than the same bill would cost on wallet. Quote with funding_source: "collect" to see the exact total before you send the customer to checkout.
What this means for your integration
- Treat
201as accepted, not done. Drive your UX off the webhook or a poll. - Handle the
processingstate — it’s the normal in-between, not an error. - Retry network failures freely with the same idempotency key.
- Expect the occasional
failed; it’s clean, and it costs you nothing. - Keep enough float to cover the customer total (bill + fees), or the payment is rejected up front.