Schedule Bill Payments
Securely store cards and pay bills with the Elements SDK. Card numbers live only inside a BillerAPI-hosted iframe — they never reach your servers, your bundle, or any postMessage. Both flows launch with a server-minted pay_token.
Two payment flows
| Flow | What it does | onSuccess first arg |
|---|---|---|
| .addPaymentMethod() | Collect + tokenize a card. No money moves. | payment_method_id |
| .pay() | Pay a specific bill with a stored or new card. | payment_attempt_id |
Step 1 — Mint a pay_token (server-side)
Mirror the link-token flow. Call POST /v1/pay-tokens from your server with your client credentials. The token is short-lived (15 minutes) and scoped to a single client_user_id with an optional bill_id, amount, or pre-bound payment method.
curl -X POST https://sandbox.api.billerapi.com/v1/pay-tokens \
-H "Authorization: Bearer $BILLERAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_user_id": "user_42",
"bill_id": "bill_123",
"amount": { "value": 13160, "currency": "USD" }
}'
# => {
# "success": true,
# "pay_token": "payt_...",
# "pay_token_id": "...",
# "expires_at": "2026-06-28T12:15:00Z"
# }client_id is authoritative from auth context
client_id is resolved from your authenticated request, never trusted from the request body. Amount and bill scope on the token are enforced at payment time — a /payments call whose bill_id does not match the token's scope is rejected.Step 2 — Launch the flow (browser)
Pass the pay_token to addPaymentMethod or pay. The hosted iframe validates the token, collects the card, and tokenizes it. Your code never sees the PAN.
import { BillerApiElements } from 'billerapi-js';
const elements = new BillerApiElements({ clientId: 'your_client_id', environment: 'sandbox' });
elements.addPaymentMethod({
payToken: 'payt_xxx',
onSuccess: (paymentMethodId, metadata) => {
// Stored card — last_4 + card_brand are safe to display
console.log('Saved', paymentMethodId, metadata.card_brand, metadata.last_4);
},
onExit: (error) => {
if (error) console.error(error.code, error.message);
},
}).open();The PCI boundary
Card data never crosses postMessage
postMessage, never reach your backend, and never touch BillEBox. The SDK only ever receives a tokenized payment_method_id plus display-safe last_4 and card_brand.The hosted card form collects exactly three fields — card number, expiry, and CVV. No cardholder name or ZIP is collected, and the card is tokenized through Vault before any identifier leaves the iframe.
Pay is sandbox-first
Money movement is gated
.pay() flow is built and testable in sandbox today. Live payment execution is gated behind the payment-execution backend; when execution is disabled the hosted page renders a clear sandbox state instead of charging a card. Build and QA against sandbox now — going live is a configuration flip, not a code change. .addPaymentMethod() (no money movement) is available end-to-end.Webhooks are the source of truth
Confirm payments server-side
onSuccess means the attempt was submitted, not that it settled. A payment_attempt_id is not a paid bill. Wait for the pay.succeeded webhook (or pay.failed) before fulfilling. SDK callbacks run in the browser and can be lost.See the webhook confirmations guide for the exact pay.* event states, and the Webhooks guide for registering an endpoint and verifying signatures.