# Link Integration Guide

Connect your users to their biller accounts in 3 steps: create a link token, open Elements, and exchange the token for persistent access.

## 1. Create a Link Token

Your server creates a short-lived link token that identifies the user and the biller they want to connect. Send this token to your frontend to initialize Elements.

### 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();
// Send link_token to your frontend
```

**Python**

```python
import requests
import os

response = requests.post(
    'https://sandbox.api.billerapi.com/v1/link-tokens',
    headers={
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $BILLERAPI_API_KEY',
    },
    json={
        'client_user_id': 'user_123',
        'biller_id': 'sb_utility',
        'consents': ['bills:read'],
    },
)
link_token = response.json()['link_token']
# Send link_token to your frontend
```

**Go**

```go
body := map[string]interface{}{
    "client_user_id": "user_123",
    "biller_id":      "sb_utility",
    "consents":       []string{"bills:read"},
}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest("POST",
    "https://sandbox.api.billerapi.com/v1/link-tokens",
    bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("BILLERAPI_API_KEY"))

resp, _ := http.DefaultClient.Do(req)
// Parse link_token from response and send to frontend
```

### Request body

**JSON**

```json
{
  "client_user_id": "string",
  "biller_id": "string (optional)",
  "consents": ["bills:read"]
}
```

### Response

**JSON**

```json
{
  "success": true,
  "link_token": "string",
  "expires_at": "string"
}
```

> **Note**
> Link tokens expire after 30 minutes. Create a new one for each linking session — do not cache or reuse them.

## 2. Open the hosted Elements flow

Pass the link token to Elements on your frontend. The SDK handles credential entry, account discovery, and consent — then returns a public token in the `onSuccess` callback.

### 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: linkToken, // from your server
  onSuccess: async (publicToken, metadata) => {
    console.log('Account linked!', metadata.institution_name);

    // Exchange the public token on your server
    const res = await fetch('/api/exchange-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ publicToken }),
    });
    const { account_link_id, access_token } = await res.json();
    // Store account_link_id for server reads. Keep access_token only if you
    // use an endpoint that explicitly supports link-scoped authentication.
  },
  onExit: (error) => {
    if (error) console.error('Error:', error.code);
  },
});

handler.open();
```

### React component example

**React**

```javascript
import React, { useState, useCallback, useRef } from 'react';
import { BillerApiElements } from 'billerapi-js';

const LinkButton = ({ userId, onSuccess }) => {
  const [isLoading, setIsLoading] = useState(false);
  const elementsRef = useRef(
    new BillerApiElements({
      clientId: process.env.NEXT_PUBLIC_BILLERAPI_CLIENT_ID,
      environment: 'sandbox',
    })
  );

  const handleLink = useCallback(async () => {
    setIsLoading(true);
    try {
      // Get link token from your server
      const response = await fetch('/api/create-link-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId })
      });
      const { linkToken } = await response.json();

      // Open the hosted Elements flow
      const handler = elementsRef.current.connect({
        linkToken,
        onSuccess: async (publicToken, metadata) => {
          await fetch('/api/exchange-token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ publicToken }),
          });
          onSuccess?.(metadata);
        },
        onExit: (error) => {
          if (error) console.error(error.code);
          setIsLoading(false);
        },
      });
      handler.open();
    } catch (error) {
      console.error('Error creating link token:', error);
      setIsLoading(false);
    }
  }, [userId, onSuccess]);

  return (
    <button
      onClick={handleLink}
      disabled={isLoading}
      className="bg-primary text-primary-foreground px-6 py-3 rounded-lg"
    >
      {isLoading ? 'Connecting...' : 'Link Account'}
    </button>
  );
};

export default LinkButton;
```

> **Tip — Sandbox test credentials**
> Use account number `4242424242` with any username and password. This always succeeds. See the [Getting Started](/docs/guides/getting-started) guide for the full sandbox reference table.

## 3. Exchange the Public Token

Send the public token from the `onSuccess` callback to your server, then exchange it for a long-lived access token. Store the access token securely — you'll use it for all subsequent API calls on this linked account.

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

**JavaScript**

```javascript
const response = await fetch(`https://sandbox.api.billerapi.com/v1/link-tokens/${encodeURIComponent(publicToken)}/exchange`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $BILLERAPI_API_KEY',
  },
  body: JSON.stringify({}),
});
const { access_token, biller_id, account_link_id } = await response.json();
// Store access_token securely. account_link_id is what you pass to
// GET /v1/bills?account_link_id=... and every other read endpoint.
```

**Python**

```python
response = requests.post(
    f'https://sandbox.api.billerapi.com/v1/link-tokens/{quote(public_token, safe="")}/exchange',
    headers={
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $BILLERAPI_API_KEY',
    },
    json={},
)
data = response.json()
access_token = data['access_token']
# Store account_link_id for server-side bill listing. Keep access_token only
# for endpoints that explicitly support link-scoped authentication.
```

**Go**

```go
body := map[string]string{}
jsonBody, _ := json.Marshal(body)

req, _ := http.NewRequest("POST",
    "https://sandbox.api.billerapi.com/v1/link-tokens/"+url.PathEscape(publicToken)+"/exchange",
    bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("BILLERAPI_API_KEY"))

resp, _ := http.DefaultClient.Do(req)
// Parse access_token from response
```

### Request body

**JSON**

```json
{
  "public_token": "string"
}
```

### Response

**JSON**

```json
{
  "success": true,
  "access_token": "string",
  "biller_id": "string",
  "account_link_id": "string",
  "link_id": "string"
}
```

> **Note**
> Public tokens are single-use and expire after 30 minutes. Always exchange them immediately after receiving the `onSuccess` callback.

## After the exchange: the async contract

`onSuccess` confirms the **connection** — the public token is exchangeable immediately. Bills arrive **asynchronously**: the first retrieval run starts right after the exchange and typically takes a minute or two. Do not poll for bills in the exchange response; listen for webhooks instead:

- `bill.created` — fires once per bill as the first retrieval run (and every later scheduled run) lands bills.
- `connection.ready` — fires exactly once per link when its first retrieval run completes successfully. Use it to flip "Syncing your bills…" UX to a ready state without polling.

If the user **backgrounds the flow** (chooses "continue in background" on the hosted connect page instead of waiting for account discovery), your `onSuccess` callback never fires. BillerAPI finishes the flow server-side — all discovered accounts are auto-selected and the public token is exchanged internally — and completion arrives via the `link_token.completed` webhook instead, carrying the created `link_id`. Treat that `link_id` exactly like the one you would have received from `POST /v1/link-tokens/:public_token/exchange`; the same `bill.created` and `connection.ready` events follow.

> **Note**
> Handle both completion paths: interactive (`onSuccess` + your own exchange call) and backgrounded (`link_token.completed` webhook, no exchange needed). Payload shapes and envelope examples are in the [Webhooks reference](/docs/api/webhooks).

## Embedding hosted Connect on your domain

The hosted Connect page ships with a strict `frame-ancestors` Content-Security-Policy: by default only BillerAPI first-party domains may frame it. To embed it in an iframe on your own site, **register your site's origin first** — otherwise the browser blocks the iframe before any script runs and the widget renders blank.

- **Portal:** [Settings → Embed Domains](/settings) — add your origin and save.
- **API:** `PUT /v1/iam/clients/:id` with `{ "allowed_embed_origins": ["https://app.example.com"] }` (a replacement list — include existing origins).

Origins must be **exact**: `https://` only (plain `http` is allowed just for `localhost`), no wildcards, no paths, and the scheme and port must match the embedding page exactly. Changes take effect within about 30 seconds.

> **Note**
> The dynamic policy is resolved from your **link token**: the embedded page URL must carry `linkToken` so BillerAPI can look up your registered origins before the page is served. The Elements SDK does this for you; if the widget stays blank on your site, check the browser console — a `frame-ancestors` CSP error (and, after ~15s, an SDK hint) means the embedding origin is not registered yet.

## Related

- [Link API Reference](/docs/api/link-sessions) — Full endpoint documentation
- [Elements SDK Guide](/docs/guides/elements-sdk) — Hosted flow configuration and options
- [Retrieve Bills Guide](/docs/guides/retrieve-bills) — Fetch bills after linking
