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

Demo video: The hosted Connect flow the Elements SDK opens: consent, authenticate, link

## 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**

```bash
npm install billerapi-js
```

**yarn**

```bash
yarn add billerapi-js
```

**pnpm**

```bash
pnpm add billerapi-js
```

**Script tag (S3)**

```html
<!-- UMD global: window.BillerApiElements -->
<!-- Always pin an exact version + SRI hash. Required next to card/PAN/CVV fields. -->
<!-- Paste the real hash printed by `bb cdn publish-elements` (scripts/cdn/cdn-publish.ts). -->
<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>"
  crossorigin="anonymous"
></script>
<!-- Sandbox/testing ONLY: the v1 alias tracks the latest 1.x with no SRI. Never in production: -->
<!-- https://bb-prod-cdn-s3-elements.s3.us-east-1.amazonaws.com/elements/v1/index.umd.js -->
<script>
  const elements = new BillerApiElements({
    clientId: 'your_client_id',
    environment: 'sandbox',
  });
</script>
```

> **Note — Availability**
> The npm packages are published as [`billerapi-js`](https://www.npmjs.com/package/billerapi-js) and [`billerapi-react`](https://www.npmjs.com/package/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.

> **Warning — 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.

> **Note — 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.

**JavaScript**

```javascript
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();
```

**React**

```react
import { ElementsProvider, useConnect } from 'billerapi-react';

function App() {
  return (
    <ElementsProvider
      config={{
        clientId: process.env.NEXT_PUBLIC_BILLERAPI_CLIENT_ID,
        environment: 'sandbox',
      }}
    >
      <LinkButton linkToken="lt_xxx" />
    </ElementsProvider>
  );
}

function LinkButton({ linkToken }) {
  const { open, ready } = useConnect({
    linkToken,
    onSuccess: async (publicToken) => {
      await fetch('/api/exchange-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ public_token: publicToken }),
      });
    },
  });

  return (
    <button onClick={open} disabled={!ready}>
      Link your account
    </button>
  );
}
```

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

| 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` | No success value | Unavailable; returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE |
| `.status()` | `discoveryRunId / linkId` | — (inline; onComplete) | Show a run's live progress |

**JavaScript**

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

> **Note — Accept payments guide**
> For available payment-method storage, the reserved Pay contract, minting a `pay_token`, and the PCI boundary, see the [Store Payment Methods](/docs/guides/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`.

**link_token**

```bash
# 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": "..." }
```

**pay_token**

```bash
# Payment flows — pay_token (15min TTL, scoped to a user/bill)
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" }'

# => { "success": true, "pay_token": "payt_...", "expires_at": "..." }
```

> **Warning — 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 single `client_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**

```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**

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

> **Warning — 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](/docs/guides/webhook-confirmations) for the exact callback-to-webhook reconciliation path, and the [Webhooks guide](/docs/guides/webhooks) 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) |

## Related

- [Store Payment Methods](/docs/guides/accept-payments) — pay_token, Add Payment Method, and the reserved Pay contract
- [Webhooks Guide](/docs/guides/webhooks) — The authoritative source of flow outcomes
- [Webhook Confirmations](/docs/guides/webhook-confirmations) — Map SDK callbacks to signed outcomes
- [Getting Started](/docs/guides/getting-started) — End-to-end integration walkthrough
