Elements SDK

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-js

Availability

The npm packages are published as 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

npm currently serves 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

Using React? Install 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. The .pay() factory is reserved: it returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE in sandbox and production and does not create an attempt.

FlowLaunch tokenonSuccess first argUse for
.connect()link_tokenpublic_tokenLink a biller account
.addPaymentMethod()pay_tokenpayment_method_idSecurely store a card
.pay()pay_tokenNo success valueUnavailable; returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE
.status()discoveryRunId / linkId— (inline; onComplete)Show a run's live progress
JavaScript
// 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();

// Bill-payment execution is unavailable in sandbox and production.
// The reserved .pay() flow returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE.

Accept payments guide

For available payment-method storage, the reserved Pay contract, minting a pay_token, the PCI boundary, and the Add-Payment-Method vs Pay split — see the Store Payment Methods 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.

client_id is never trusted from the browser

The mint endpoints derive 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.

JavaScript
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().

JavaScript
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

SDK callbacks (onSuccess, onExit) are UX hints. They run in the user's browser and can be lost to a closed tab or dropped network. For Connect, treat the signed link.completed webhook as the authoritative record before provisioning linked-account access. Pay execution is unavailable and produces no payment attempt or pay.* event. pay.succeeded and pay.failed are reserved future event names only; do not build fulfillment against them until Pay launches.

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.

PropertyTypeRequiredDescription
clientIdstringYesYour BillerAPI client ID
environment'sandbox' | 'production'NoTarget environment (auto-detected if omitted)
baseUrlstringNoCustom base URL; per-flow hosted paths are derived from it
apiUrlstringNoCustom API URL (overrides environment default)
connectUrlstringNoExplicit Connect URL (back-compat; only short-circuits the connect flow)

Related

Was this page helpful?