Billy billy Beta Docs

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-Key header is required. Anything off is a 422 with 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 a payment.succeeded webhook fires.
  • Failure. The hold is released straight back to your available balance. The payment becomes failed with a reason, and a payment.failed webhook 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 webhookspayment.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 201 as accepted, not done. Drive your UX off the webhook or a poll.
  • Handle the processing state — 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.