# Connection flow fundamentals

How BillerAPI links your users to their biller accounts.

Concepts

A BillerAPI integration has two components. The first is **end-user biller account linking**, which your users complete through the hosted Elements flow. The second is **data retrieval** — reading bills and insights on the accounts your users have linked.

The lifecycle below walks a single connection from the moment your user taps *Connect a biller* to the moment bills and webhook events start arriving. Each stage names the actor responsible for it: your client, your server, or BillerAPI.

## Who does what

- **Client** — Your web or mobile app. Initiates linking and opens the hosted Elements flow. Never handles biller credentials. (Stages 2 · 3)
- **Your server** — Holds your API key. Creates link tokens, exchanges the public token, and stores the resulting access token. (Stages 1 · 4 · 5)
- **BillerAPI** — Hosts the connect flow, authenticates the user with their biller, retrieves bills, and delivers webhook events. (Stages 1 · 3 · 4 · 5)

## Connection lifecycle

Five stages, in order. Stages 1 and 4 run on your server with your API key; stages 2 and 3 run in the browser; stage 5 is ongoing.

### Stage 01 · Server-side — Create a link token

Actor: Your server

Your user initiates biller linking on your client. Your client calls your backend, and your backend creates a link token with `POST /v1/link-tokens`, passing `client_user_id`, an optional `biller_id`, and the `consents` your use case requires. Return the resulting `link_token` to your client.

**POST /v1/link-tokens**

**JavaScript**

```javascript
const response = await fetch('https://sandbox.api.billerapi.com/v1/link-tokens', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $BILLERAPI_API_KEY',
  },
  body: JSON.stringify({
    client_user_id: 'user_123',
    biller_id: 'sb_utility',
    consents: ['bills:read'],
  }),
});
const { link_token } = await response.json();
```

> **Note — Link tokens expire after 30 minutes**
> Create one per link attempt, hand it straight to the client, and never cache or reuse it. An expired or reused token fails the flow.

### Stage 02 · Client-side — Open Elements

Actor: Client

Initiate the hosted flow with `elements.connect({ linkToken, onSuccess, onExit })` from `billerapi-js`. Elements renders in an overlay on your page — your app keeps the surrounding experience, BillerAPI owns everything inside the frame.

**Initialize and open Elements**

**JavaScript**

```javascript
import { BillerApiElements } from 'billerapi-js';

const elements = new BillerApiElements({
  clientId: 'your_client_id',
  environment: 'sandbox',
});

const handler = elements.connect({
  linkToken,
  onSuccess: async (publicToken, metadata) => {
    // send publicToken to your server for exchange
  },
  onExit: (error) => { if (error) console.error(error.code); },
});
handler.open();
```

### Stage 03 · Hosted by BillerAPI — Your user completes the hosted flow

Actor: BillerAPI

Inside the frame, the user selects their biller, reviews and grants the consent scopes, picks a service region if that biller is region-gated, authenticates with the biller, clears MFA if required, and confirms which discovered accounts to link. When the flow finishes, `onSuccess` fires with a `public_token`.

The most common path through the hosted Elements connect flow, as five frames titled "Connect a biller":

- **01 · Select biller** — Find your biller. Search supported billers. Search term: `spring`. Springfield Utility (ELECTRIC · IL, selected), Springfield Water (WATER · IL), Spring Broadband (INTERNET · US). Button: Continue.
- **02 · Consent** — Share your bill data. Springfield Utility → Acme App. Bills — Amounts, due dates, statements (`bills:read`). Account details — Biller name, masked account, status. Button: Agree and continue.
- **03 · Credentials** — Log in to Springfield Utility. Entered here only — never sent to Acme App. Username and Password: ••••••••••••. Verify (MFA) — if your biller asks, we'll prompt for a one-time code next. Button: Log in.
- **04 · Select accounts** — Choose accounts to link. 2 accounts found at Springfield Utility. Electric ••••4242 (checked), Gas ••••8811. Button: Link account.
- **05 · Success** — Account linked. Springfield Utility. Account ••••4242. Button: Done.

The most common path. Region selection appears between consent and credentials for region-gated billers, and an MFA or security-question step appears after login when the biller requires one.

The full sequence, including every conditional step, is listed in [Inside the hosted flow](#inside-the-hosted-flow).

> **Tip — Sandbox test credentials**
> Account number `4242424242` with any username and password always succeeds — that is the happy path, with no challenge screens. To exercise MFA, use `4000000003` and enter code `123456` when the flow prompts. The full set of magic account numbers, including the error and multi-account scenarios, is in the [Getting Started sandbox reference](/docs/guides/getting-started#sandbox-reference).

> **Warning — Credentials never reach your servers**
> Biller usernames, passwords, one-time codes, and security-question answers are entered inside the hosted flow only. Your application never sees, transmits, or stores them.

### Stage 04 · Server-side — Exchange the public token

Actor: Your server

Send the `public_token` to your backend and exchange it with `POST /v1/link-tokens/:public_token/exchange`. You get back an `access_token`, the `biller_id`, and an `account_link_id` — store the last two against your user.

**POST /v1/link-tokens/:public_token/exchange**

**JavaScript**

```javascript
const exchange = await fetch(
  `https://sandbox.api.billerapi.com/v1/link-tokens/${encodeURIComponent(publicToken)}/exchange`,
  { method: 'POST', headers: { Authorization: 'Bearer $BILLERAPI_API_KEY' } },
);
const { access_token, biller_id, account_link_id } = await exchange.json();
```

The link session that produced the token is readable for auditing and support:

**The link_session object**

**JSON**

```json
{
  "id": "ls_9f2c41d8a7b3",
  "object": "link_session",
  "client_user_id": "user_123",
  "biller_id": "sb_utility",
  "consents": ["bills:read"],
  "status": "succeeded",
  "account_link_id": "al_5e8d02c9f1a4",
  "expires_at": "2026-08-23T18:30:00Z"
}
```

### Stage 05 · Ongoing — Read bills and receive webhooks

Actor: BillerAPI

With an `account_link_id` you can fetch bills for the linked account at any time.

**HTTP**

```bash
GET /v1/bills?account_link_id=al_5e8d02c9f1a4
Authorization: Bearer $BILLERAPI_API_KEY
```

Linking is only half synchronous. Everything that happens after the flow closes — the connection settling, new statements arriving, a link going stale — is delivered asynchronously by [webhooks](/docs/guides/webhooks). Subscribe to the events your integration needs:

- `link.connected`
- `bill.created`
- `link.disconnected`

> **Note — Don't poll for the first bill**
> Bills are retrieved after the flow closes, so an immediate `GET /v1/bills` can return an empty list. Wait for `bill.created`. See [Webhook Delivery](/docs/concepts/webhook-delivery) for signature verification and retry semantics.

## Inside the hosted flow

These are the steps your user moves through inside the Elements modal, in order. Steps adapt to the biller and to the session — only region-gated billers show the region step, a biller without MFA skips verification, a single-account biller skips account selection, and update or repair sessions skip consent.

| Step | Description |
| --- | --- |
| 1 Select biller | User searches or picks their biller from supported billers. |
| 2 Provide consent | User agrees to share the requested data (consent scopes shown). Skipped in update/repair mode. |
| 3 Select region | Shown only for region-gated billers (for example Eversource CT/MA/NH). It sits between consent and credentials, because the region decides which login the biller presents. |
| 4 Log in to biller | User authenticates with their biller credentials. |
| 5 Verify (MFA) | Only if the biller requires it: one-time code or security question. |
| 6 Select accounts | User picks which discovered accounts to link — this is the confirm-link step. |
| 7 Success | Confirmation screen; the flow returns control to your app. |

## Data permissions

Consent scopes decide what BillerAPI may read on a linked account. They are requested on the link token and shown to the user on the consent step.

| Data | Scope | Description |
| --- | --- | --- |
| Bills | `bills:read` | Bill amounts, due dates, statement PDFs on linked accounts. |
| Account details | granted on link | Biller name, masked account identifier, link status. |
| Insights | derived from linked data | Usage and payment insights computed from linked accounts, where enabled. No separate scope is requested. |

Request only the scopes your use case needs. Users see the requested scopes on the consent step and must re-link to expand them.

## How BillerAPI authenticates

**Credentials are entered in the hosted flow only.** Biller usernames and passwords are collected inside the Elements frame and never touch your servers — you never receive, proxy, or store them.

**Region selection precedes the login, not the biller choice.** A small number of billers serve several regions under one brand (Eversource CT/MA/NH, for example). For those, the hosted flow inserts a region step after consent and before credentials, because the region determines which login form the biller presents. Every other biller goes straight from consent to credentials.

**MFA and security questions are handled inline.** When a biller challenges the login with a one-time code or a security question, the hosted flow prompts the user in place and continues the session once it clears. No callback into your application is required.

**Sandbox has a deterministic credential.** Account number `4242424242` with any username and password always succeeds, so you can build and test the entire lifecycle before you have a real biller account.

- Next: [Link Integration Guide](/docs/guides/link-account) →
- Reference: [Elements SDK](/docs/guides/elements-sdk) →
- Async: [Set Up Webhooks](/docs/guides/webhooks) →
