npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

billerapi-js

v1.0.1

Published

BillerAPI Elements — unified iframe SDK for Connect, Add Payment Method, and Pay flows

Readme

billerapi-js

The unified, Plaid-style SDK for embedding BillerAPI Elements — secure, iframe-hosted flows that keep credentials, card numbers, and CVV entirely out of your JavaScript context.

Three flows, one SDK:

| Flow | Factory | Hosted page | onSuccess first arg | |---|---|---|---| | Connect (account linking) | .connect() | /connect | publicToken | | Add a payment method | .addPaymentMethod() | /elements/add-payment-method | paymentMethodId | | Pay a bill | .pay() | /elements/pay | paymentAttemptId |

Successor to @billerapi/connect-sdk, which is now deprecated — new integrations should use this package. The BillerAPI and BillButler aliases are exported for back-compat, so existing @billerapi/connect-sdk consumers keep working: new BillButler(...).create(opts) maps to .connect(opts).

Installation

Elements is distributed as a UMD <script> bundle from BillerAPI's S3 CDN — no npm install required (npm/public-CDN publishing is deferred). Drop it in and use the window.BillerApiElements global:

<script
  src="https://bb-prod-cdn-s3-elements.s3.us-east-1.amazonaws.com/elements/1.0.0/index.umd.js"
  integrity="sha384-<paste the hash printed by `bb cdn publish-elements` — also shown in the Elements SDK guide>"
  crossorigin="anonymous"
></script>

Always use the immutable versioned path (elements/1.0.0/…) with the matching integrity="sha384-…" SRI hash — this is the only form safe to embed next to card/PAN/CVV fields, because SRI lets the browser reject a swapped bundle. The real hash is generated at publish time by scripts/cdn/cdn-publish.ts and printed by bb cdn publish-elements; the Elements SDK guide lists the current version and hash.

For sandbox/testing only, the moving v1 alias (elements/v1/index.umd.js) tracks the latest 1.x without SRI — never use it in production.

Quick start

import { BillerApiElements } from 'billerapi-js';

const elements = new BillerApiElements({
  clientId: 'your-client-id',
  environment: 'sandbox', // or 'production'; auto-detected from hostname when omitted
});

1. Connect (account linking)

Mint a link_token server-side (POST /v1/link/token/create), then:

elements
  .connect({
    linkToken: 'link-token-from-your-backend',
    onSuccess: (publicToken, metadata) => {
      if (metadata.completion_mode === 'update') {
        // Update/repair completion (link token minted with `update`): the Link
        // already exists, `publicToken` is an EMPTY string — do NOT exchange it.
        // Just treat this as "the user finished re-authenticating".
        return;
      }
      // First-time connect: exchange publicToken server-side for an access token.
      console.log('linked', publicToken, metadata.institution_name);
    },
    onExit: (error) => {
      if (error) console.error(error.code, error.message);
    },
  })
  .open();

2. Add a payment method

Mint a pay_token server-side (POST /v1/pay/token/create), then:

elements
  .addPaymentMethod({
    payToken: 'pay-token-from-your-backend',
    onSuccess: (paymentMethodId, metadata) => {
      console.log('saved card', paymentMethodId, metadata.last_4, metadata.card_brand);
    },
  })
  .open();

3. Pay a bill

elements
  .pay({
    payToken: 'pay-token-from-your-backend',
    billId: 'bill_123', // optional — the pay_token may already be bill-scoped
    onSuccess: (paymentAttemptId, metadata) => {
      console.log('payment started', paymentAttemptId, metadata.bill_id);
    },
  })
  .open();

Theming

The developer-facing theme is camelCase; the SDK translates it to the snake_case wire shape exactly once before posting it to the hosted page.

elements.connect({
  linkToken,
  onSuccess,
  theme: {
    primaryColor: '#1D4ED8',   // 6-digit hex
    mode: 'system',            // 'light' | 'dark' | 'system'
    borderRadius: 'lg',        // 'sm' | 'md' | 'lg'
    clientLogoUrl: 'https://cdn.example/logo.png',
  },
});

// Back-compat: a bare mode string is also accepted.
elements.connect({ linkToken, onSuccess, theme: 'dark' });

API

new BillerApiElements(config)

interface ElementsConfig {
  clientId: string;                       // required, /^[a-zA-Z0-9_-]+$/
  environment?: 'sandbox' | 'production'; // auto-detected from hostname; SSR -> 'sandbox'
  baseUrl?: string;                       // overrides per-flow path derivation base
  apiUrl?: string;                        // overrides env API URL
  connectUrl?: string;                    // explicit connect URL (connect flow only, back-compat)
}

Methods: .connect(opts), .addPaymentMethod(opts), .pay(opts) (each returns an ElementHandler), plus .getConfig() and .destroy().

ElementHandler

interface ElementHandler {
  open(): Promise<void>; // idempotent; a missing required token routes to onExit
  close(): void;         // idempotent
  isOpen(): boolean;
}

Only one modal can be in flight: calling any factory closes the previous handler.

Options

Every flow accepts onSuccess (required), onExit, onLoad, onEvent, and theme. connect requires linkToken; addPaymentMethod and pay require payToken (pay also accepts an optional billId).

Connect events (onEvent)

The connect flow emits a stable, namespaced event vocabulary through onEvent — the conversion-funnel backbone. Names are connect/<verb>; every event carries snake_case metadata (it crosses the postMessage wire). Types ship as ConnectEventName, ConnectViewName, ConnectEventMetadataByName, and the ConnectEventHandler handler type. onEvent stays widened, so a plain (eventName: string, metadata) => void handler still type-checks (ad-hoc events such as biller_onboarding_requested continue to flow through unchanged).

Every event includes: link_session_id (the link token id, used as the v1 session key), timestamp (ISO-8601), and — once known — biller_id, biller_name, view_name, and mode.

mode (ConnectFlowMode) is 'connect' for a first-time connect or 'update' for a repair / re-authentication of an existing Link (its credentials went stale, the session expired, MFA is required again, or consent lapsed). In update mode the biller is already known, the consent step is skipped, and the account step re-confirms the existing selection. connect/open with mode: 'update' marks an update flow starting. Segment your repair funnel off mode — no new event names are introduced.

| Event | When | Extra metadata | |-------|------|----------------| | connect/open | Page initialized after INIT | — | | connect/view | Every pane/step transition | view_name | | connect/search | Settled (debounced) biller search | query | | connect/biller_selected | A biller was chosen | biller_id, biller_name | | connect/credentials_submitted | Credentials submitted for login | — | | connect/mfa_required | An MFA challenge is required | mfa_type (when known) | | connect/otp_submitted | An OTP / MFA code was submitted | — | | connect/input_required | Flow left a processing state, now needs input | view_name | | connect/authenticated | Biller-side login verified (fires once, often well before completion) | — | | connect/backgrounded | User chose to continue the connect in the background (followed by a clean EXIT, no error) | view_name (pane the choice was made from) | | connect/error | An error surfaced | error_code, will_retry (when true, the backend keeps retrying a backgrounded transient failure — treat as pending, not terminal) | | connect/exit | The user bailed | status (pane they left) | | connect/handoff | Public token handed to the parent | — |

view_name / status values: biller_selection, consent, region, credentials, mfa, account_selection, success, failure, loading.

connect/error carries a stable, machine-readable error_code (typed as ConnectErrorCode, never localized UI copy) — key your analytics/error handling off these values, not the user-facing message:

| error_code | Meaning | |--------------|---------| | credentials_rejected | The biller rejected the submitted credentials | | mfa_invalid | The submitted OTP / MFA code was wrong | | mfa_expired | The OTP / MFA code (or challenge) timed out or expired | | mfa_locked_out | Too many MFA attempts; the account is locked | | account_discovery_failed | Login succeeded but the account list couldn't be read | | connection_lost | The realtime stream dropped and couldn't reconnect | | link_expired | The link token is no longer valid | | internal_error | A generic client/network or unclassified backend fault |

Deprecated (pre-taxonomy) events

The hosted page still emits a few ad-hoc events that predate this taxonomy, kept for back-compat. Prefer the connect/* events above; these will be removed in a future major:

  • STEP_CHANGE — superseded by connect/view.
  • CLOSE_CONFIRM_SHOWN / CLOSE_CONFIRM_DISMISSED — internal close-confirm dialog telemetry; no connect/* replacement (use connect/exit).
elements.connect({
  linkToken,
  onSuccess: (publicToken, metadata) => exchange(publicToken),
  onEvent: (eventName, metadata) => {
    if (eventName === 'connect/view') analytics.track('connect_view', metadata);
    if (eventName === 'connect/error') analytics.track('connect_error', metadata);
  },
});

Async connect contract

onSuccess confirms the connection — the public token it hands you is exchangeable immediately (POST /link/token/exchange). Bills arrive asynchronously: the first retrieval run starts right after the exchange. Don't poll for bills off the exchange response; listen for webhooks:

  • bill.created — fires once per bill as retrieval runs land bills.
  • connection.ready — fires exactly once per link when its FIRST retrieval run completes successfully. Flip "Syncing your bills…" UX on this, not on a timer.

Backgrounded flows. If the user chooses "continue in background" on the hosted connect page instead of waiting out account discovery, onSuccess never fires. BillerAPI completes the flow server-side (all discovered accounts auto-selected, public token exchanged internally — it is never exposed on this path) and completion arrives via the link_token.completed webhook instead, carrying the created link_id. Treat that link_id exactly like one returned by your own exchange call; the same bill.created and connection.ready events follow.

Handle both completion paths:

| Path | Completion signal | Your exchange call | |------|-------------------|--------------------| | Interactive | onSuccess(publicToken) | Required (POST /link/token/exchange) | | Backgrounded | link_token.completed webhook | Not needed (done server-side) |

Payload shapes and full #1555 envelope examples live in the hosted Webhooks reference (/docs/api/webhooks) and docs/webhooks/biller-api-events.md.

Security

  • Iframe isolation — all sensitive UI (including card number / CVV entry) lives in the BillerAPI-hosted page, never in your context.
  • Origin-locked postMessage — the SDK locks the expected origin at open(), rejects any inbound message from another origin, and never posts to '*'.
  • Hardened protocol v1 — every message carries protocol_version: 1. The SDK sends the canonical BILLERAPI_* prefix and accepts the legacy BILLERAPI_* prefix inbound for back-compat.
  • PAN / CVV never cross postMessage and never reach any first-party backend other than the gateway tokenization endpoint.

Browser support

Chrome 60+, Firefox 55+, Safari 12+, Edge 79+.

License

MIT