Store Payment Methods
Securely store cards with the Elements SDK. Card numbers live only inside a BillerAPI-hosted iframe — they never reach your servers, your bundle, or any postMessage. Bill-payment execution is not available; attempts return 501 PAYMENT_EXECUTION_NOT_AVAILABLE.
Available and reserved flows
| Flow | What it does | onSuccess first arg |
|---|---|---|
| .addPaymentMethod() | Collect + tokenize a card. No money moves. | payment_method_id |
| .pay() | Reserved bill-payment flow. It currently returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE; no payment is initiated. | No success value |
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 part of the reserved payment contract. Payment initiation currently terminates with 501 PAYMENT_EXECUTION_NOT_AVAILABLE before money movement.Step 2 — Launch card storage (browser)
Pass the pay_token to addPaymentMethod. The hosted iframe validates the token, collects the card, and tokenizes it. Your code never sees the PAN. The pay example below documents the reserved contract only; it is not an executable payment path.
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.
Payment execution is unavailable
Money movement is gated
.pay() flow does not execute payments in sandbox or production. It returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE, and the hosted page renders an unavailable state instead of charging a card. Launch requires compliance approval, isolated operations, verified biller support, staged evidence, and explicit sign-off; it is not a configuration-only change. .addPaymentMethod() (no money movement) remains available end-to-end.Future payment outcomes
Confirm payments server-side
onSuccess will mean the attempt was submitted, not that it settled. A payment_attempt_id will not by itself prove a paid bill. Integrations must wait for an authoritative pay.succeeded or pay.failed webhook. 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.