Elements SDK
billerapi-js is the unified drop-in SDK for every BillerAPI flow — Connect, Add Payment Method, and Pay. One client, three secure iframes. Credentials and card numbers never touch your code.
Installation
Install the browser SDK from npm for bundled apps, or drop in the hosted UMD build with a single <script> tag for no-build pages.
npm install billerapi-jsAvailability
billerapi-js and billerapi-react. The script-tag drop-in is served from an S3 bucket over HTTPS (bb-{env}-cdn-s3-elements.s3.us-east-1.amazonaws.com). Use npm for app bundles and the script tag for no-build prototypes. External customer rollout still follows your account's go-live and embed-security gates.Reviewed 3.x release is not published
billerapi-js@1.0.1 and billerapi-react@1.0.1. The reviewed next release intent is the coordinated 3.0.0 pair. Keep existing elements/v1 and immutable 1.x CDN objects unchanged until a separately authorized exact-SHA npm/CDN release.React bindings
billerapi-react for the <ElementsProvider> component and the useConnect, useAddPaymentMethod, and usePay hooks.Quick Start
Construct one BillerApiElements instance with your client ID, then launch any flow with a server-minted token.
import { BillerApiElements } from 'billerapi-js';
// 1. One client for every flow
const elements = new BillerApiElements({
clientId: 'your_client_id',
environment: 'sandbox',
});
// 2. Launch the Connect flow with a link_token from your server
const handler = elements.connect({
linkToken: 'lt_xxx', // from POST /v1/link-tokens
onSuccess: (publicToken, metadata) => {
// Send publicToken to your server to exchange for an access token
console.log('Linked!', metadata.account_id, metadata.institution_name);
},
onExit: (error) => {
if (error) console.error('Connect error:', error.code, error.message);
},
});
// 3. Open the secure iframe
handler.open();The four flows
The three modal factories return the same ElementHandler (open(), close(), isOpen()). Opening any flow closes the one currently in flight — only one Elements modal is ever on screen. .status() is different: it renders inline (mount()/unmount()) and is read-only — it shows an end-user the live progress of a run they kicked off, and never collects data.
| Flow | Launch token | onSuccess first arg | Use for |
|---|---|---|---|
| .connect() | link_token | public_token | Link a biller account |
| .addPaymentMethod() | pay_token | payment_method_id | Securely store a card |
| .pay() | pay_token | payment_attempt_id | Pay a bill (sandbox-first) |
| .status() | discoveryRunId / linkId | — (inline; onComplete) | Show a run's live progress |
// Add a payment method (no money moves)
elements.addPaymentMethod({
payToken: 'payt_xxx', // from POST /v1/pay-tokens
onSuccess: (paymentMethodId, metadata) => {
console.log('Saved card', paymentMethodId, metadata.card_brand, metadata.last_4);
},
}).open();
// Pay a bill
elements.pay({
payToken: 'payt_xxx',
billId: 'bill_123', // optional — pay_token may already be bill-scoped
onSuccess: (paymentAttemptId, metadata) => {
console.log('Payment submitted', paymentAttemptId, metadata.bill_id);
},
}).open();Accept payments guide
pay_token, the PCI boundary, and the Add-Payment-Method vs Pay split — see the Schedule Bill Payments guide.Server-side token minting
Every flow launches with a short-lived, scoped token minted on your server using your client credentials. The browser only ever holds the token — your API key never reaches the client. Connect uses a link_token; payment flows use a pay_token.
# Connect flow — link_token (24h TTL)
curl -X POST https://sandbox.api.billerapi.com/v1/link-tokens \
-H "Authorization: Bearer $BILLERAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "client_user_id": "user_42" }'
# => { "link_token": "sb_link_tok_...", "expires_at": "..." }client_id is never trusted from the browser
client_id from your authenticated request, not the request body. Tokens are scoped to a singleclient_user_id (and optional bill / amount) and expire quickly — mint one per flow launch, server-side.Theming
Pass 'light', 'dark', or a theme object to any flow. The SDK accepts camelCase and normalizes it to the snake_case wire shape before handing it to the hosted page.
elements.connect({
linkToken: 'lt_xxx',
theme: {
primaryColor: '#2B6CB0',
mode: 'system', // 'light' | 'dark' | 'system'
borderRadius: 'md', // 'sm' | 'md' | 'lg'
clientLogoUrl: 'https://yourapp.com/logo.svg',
},
onSuccess: (publicToken) => { /* ... */ },
});Error handling
onExit fires both on user-cancel (no argument) and on error (an ElementsError with code and message). A missing or expired launch token is reported as MODAL_OPEN_ERROR rather than throwing from open().
elements.connect({
linkToken,
onSuccess: (publicToken) => { /* ... */ },
onExit: (error) => {
if (!error) {
// user closed the modal — not an error
return;
}
switch (error.code) {
case 'MODAL_OPEN_ERROR':
// missing/expired token — mint a fresh one and retry
break;
default:
console.error(error.code, error.message);
}
},
});Webhooks are the source of truth
Confirm outcomes server-side
onSuccess, onExit) are UX hints. They run in the user's browser and can be lost to a closed tab or dropped network. Treat the matching webhook event as the authoritative record before fulfilling anything:link.completed for Connect, pay.succeeded / pay.failed for payments.See the Webhook Confirmations guide for the exact callback-to-webhook reconciliation path, and the Webhooks guide to register an endpoint and verify signatures.
Configuration
Pass an ElementsConfig object to the constructor.
| Property | Type | Required | Description |
|---|---|---|---|
| clientId | string | Yes | Your BillerAPI client ID |
| environment | 'sandbox' | 'production' | No | Target environment (auto-detected if omitted) |
| baseUrl | string | No | Custom base URL; per-flow hosted paths are derived from it |
| apiUrl | string | No | Custom API URL (overrides environment default) |
| connectUrl | string | No | Explicit Connect URL (back-compat; only short-circuits the connect flow) |