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
Drop in the UMD build from our S3 host with a single <script> tag for no-build pages. (npm packages are on the way — use the script tag today.)
npm install billerapi-jsAvailability
bb-{env}-cdn-s3-elements.s3.us-east-1.amazonaws.com). The dev and staging buckets are live now for integration testing — swap prod for dev/staging in the hostname. Production distribution ships after the embed security review. The npm packages (billerapi-js, billerapi-react) are not on the public registry yet — use the script tag above.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 three flows
Each factory returns the same ElementHandler (open(), close(), isOpen()). Opening any flow closes the one currently in flight — only one Elements modal is ever on screen.
| 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) |
// Add a payment method (no money moves)
elements.addPaymentMethod({
payToken: 'payt_xxx', // from POST /v1/pay/token/create
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 Accept 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/link/token/create \
-H "Authorization: Bearer $BILLERAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "client_user_id": "user_42" }'
# => { "link_token": "lt_...", "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 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) |