Billy billy Beta Docs

Making a payment

Post a payment to a biller. Billy handles the money and the biller settlement, and tells you the outcome by webhook.

The flow

  1. Quote the amount so you can show the customer the total, including both fees.
  2. Collect the biller's required fields, validated against the biller's field schema.
  3. Post the payment with an idempotency key.
  4. Receive webhook events as the payment settles.

Want the full picture? How a payment settles walks through exactly what Billy does with your request end to end — the checks it runs, how your float is ring-fenced and released, why posting is asynchronous, and how you learn the outcome.

Getting the fields right

This is where most first integrations trip. Every biller declares its own fields, and you send them back keyed by positionfield_1, field_2, and so on — not by name.

The biller detail response tells you what each position is:

GET /billers/01KTQCYWKEW9ZX0BS1CTHFYR8B

{
  "data": {
    "name": "PLDT",
    "fields": [
      {
        "position": 1,
        "name": "10 Digit Account Number",
        "format": "numeric",
        "width": 10,
        "required": true,
        "validation_regex": "^[0-9]{10}$"
      }
    ]
  }
}

A field at "position": 1 is sent as field_1:

{ "fields": { "field_1": "1234567890" } }
Render your form from name, validate against validation_regex and width, but send by position. Every field marked required: true must be present and match its regex, or the payment is rejected with 422 before any money moves.

Create a payment

POST /billers/{id}/payments
Authorization: Bearer $KEY
Idempotency-Key: 8f14e45f-ea28-4e1a-9a2b-1c2d3e4f5a6b

{
  "amount": 1500.00,
  "fields": { "field_1": "1234567890" },
  "external_customer_ref": "app-user-8f3a",
  "metadata": { "order_id": "ord_123" }
}

Request fields

FieldRequiredNotes
amountYesThe bill amount, as billed to your customer. Must fall within the biller's min/max. The convenience and platform fees are added on top.
fieldsYesKeyed field_1, field_2… by the biller's field positions. See above.
funding_sourceNowallet (default) or collect. See below.
external_customer_refNoOpaque, app-supplied reference for your end-user. Stored with the payment.
metadataNoArbitrary key/value passthrough, echoed back on the payment and its webhooks.

Response

{
  "data": {
    "id": "01kxjehb1aa3ztdy315q0ffbdh",
    "status": "processing",
    "funding_source": "wallet",
    "biller": { "id": "01KTQ…", "code": "PLDT", "name": "PLDT" },
    "amount": "1500.00",
    "convenience_fee": "20.00",
    "platform_fee": "25.00",
    "customer_total": "1545.00",
    "refunded_amount": "0.00",
    "currency": "PHP",
    "reference": null,
    "failure_code": null,
    "failure_reason": null,
    "created_at": "2026-07-15T08:33:17+00:00"
  }
}

The id is the payment's opaque public id — use it for every follow-up call. reference is the biller's receipt, and stays null until the bill actually posts.

What you're charged

Three components make up what you pay, reported separately at every level:

FieldWhat it is
amountThe bill. Goes to the biller (less any provider service fee).
convenience_feeThe provider's fee for the biller, charged on top of the bill. Set by the aggregator, per biller.
platform_feeBilly's fee for making the posting — a percentage of the bill plus a fixed amount, either of which may be zero. Your rate is on your agreement.
customer_totalamount + convenience_fee + platform_fee. This is what leaves your float, and what you'd charge your end-user.

Quote before you charge your customer and you'll never have to compute the fees yourself — the quote is produced by the same calculation as the payment, so the two always agree.

Wallet-funded payments (default)

The customer total is reserved from your float immediately, the payment returns processing, and Billy posts the bill in the background. On success it clears out to the biller; on failure the hold returns to you and you are made whole.

If your float can't cover it, the payment is rejected with 402 and nothing is reserved.

Billy collects (funding_source: "collect")

Use this when you'd rather Billy take the money from the end-user than draw down your float. The payment comes back awaiting_payment with a hosted checkout link:

{
  "data": {
    "id": "01kxj…",
    "status": "awaiting_payment",
    "funding_source": "collect",
    "gateway_fee": "0.00",
    "customer_total": "1545.00",
    "checkout_url": "https://checkout.magpie.im/…"
  }
}

Send the customer to checkout_url. Once they pay, Billy posts the bill automatically and your float is never touched. If the biller then rejects the bill, Billy refunds the customer's charge for you.

Collect carries a gateway_fee — the payment gateway's processing charge, surcharged to the payer — so customer_total is the grossed-up amount the customer pays at checkout, higher than the same bill on wallet (where gateway_fee is always 0.00). Quote with funding_source: "collect" to see the exact figure before you send the customer to checkout.

checkout_url is returned only on the response that creates the payment — it isn't stored. Capture it there, or you'll have to create a new payment.

Payment statuses

StatusMeaningFinal?
pendingAccepted, not yet funded.No
awaiting_paymentcollect only — waiting for the end-user to pay at checkout.No
processingFunds secured; posting to the biller.No
succeededThe biller accepted the bill. reference holds the receipt.Yes
failedCould not be completed. See failure_code and failure_reason. Money returned.Yes
refundedFully reversed after settling. See Refunds.Yes
Never treat processing as success. A bill isn't paid until the payment is succeeded and carries a reference.

Idempotency

Always send an Idempotency-Key. If a request is retried with the same key, Billy returns the original payment instead of creating a second one — safe against network retries and timeouts. See Errors & idempotency.

Reading payments back

GET /payments/{id}
GET /payments?status=succeeded&per_page=25

You only ever see your own payments; another partner's payment id returns 404.

Webhooks

Each status change is delivered to your endpoint. Verify the signature on every event, respond 2xx to acknowledge, and treat delivery as at-least-once — deduplicate on the payment id. See Webhooks.

Storing saved billers and running recurring payments is your app's responsibility today. See Saved bills & autopay for how that works and what's on the roadmap.