Accept 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/token/create 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/token/create \
-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 Webhooks guide for registering an endpoint and verifying signatures.