Link a Biller Account

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 /link/token/create
const response = await fetch('https://sandbox.api.billerapi.com/link/token/create', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $BILLERAPI_API_KEY',
  },
  body: JSON.stringify({
    client_user_id: 'user_123',
    biller_id: 'test_electric_company',
    consents: ['bills'],
  }),
});
const { link_token } = await response.json();
// Send link_token to your frontend

Request body

JSON
{
  "client_user_id": "string",
  "biller_id": "string (optional)",
  "consents": ["bills"]
}

Response

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
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 { access_token } = await res.json();
    // Store access_token — you'll need it to fetch bills
  },
  onExit: (error) => {
    if (error) console.error('Error:', error.code);
  },
});

handler.open();

React component example

React
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;

Sandbox test credentials

Use account number 4242424242 with any username and password. This always succeeds. See the 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 /link/exchange-public-token
const response = await fetch('https://sandbox.api.billerapi.com/link/exchange-public-token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer $BILLERAPI_API_KEY',
  },
  body: JSON.stringify({
    public_token: publicToken,
  }),
});
const { access_token, biller_id, link_id } = await response.json();
// Store access_token securely — use it to fetch bills

Request body

JSON
{
  "public_token": "string"
}

Response

JSON
{
  "success": true,
  "access_token": "string",
  "biller_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 /link/exchange-public-token; 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.

Related

Was this page helpful?