Schedule Bill Payments

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

FlowWhat it doesonSuccess 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

The card number and CVV live only in the BillerAPI-origin hosted iframe's local state. They are never sent over 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

The .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

If payment execution becomes available, 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.

Related

Was this page helpful?