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.
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 frontendRequest body
{
"client_user_id": "string",
"biller_id": "string (optional)",
"consents": ["bills"]
}Response
{
"success": true,
"link_token": "string",
"expires_at": "string"
}Note
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
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
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
4242424242 with any username and password. This always succeeds. See the Getting Started guide for the full sandbox reference table.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 billsRequest body
{
"public_token": "string"
}Response
{
"success": true,
"access_token": "string",
"biller_id": "string",
"link_id": "string"
}Note
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
onSuccess + your own exchange call) and backgrounded (link_token.completed webhook, no exchange needed). Payload shapes and envelope examples are in the Webhooks reference.