Expert guidance for Plaid banking API integration including Link, Auth, Transactions, Identity, and webhook handling. Invoke when user mentions Plaid, bank connections, financial data, ACH, or banking APIs.
Provide comprehensive guidance for integrating Plaid's financial data APIs to connect bank accounts, retrieve transactions, verify identities, and process ACH transfers.
Invoke when user mentions:
Purpose: Retrieve bank account and routing numbers for ACH, wire transfers, and bank-to-bank payments.
Use cases:
Purpose: Access transaction history for budgeting, expense tracking, and financial insights.
Features:
Purpose: Verify user identity through bank account ownership.
Data retrieved:
Purpose: Real-time account balance checking to prevent payment failures.
Use cases:
Purpose: Access holdings and transactions from investment accounts.
Coverage:
Purpose: Loan and credit data access.
Coverage:
Plaid Link is the client-side component that users interact with to securely connect their bank accounts. It's a modal/iframe that handles the entire authentication flow.
public_tokenpublic_token for access_token server-sideInstall:
npm install react-plaid-link
Implementation:
import { usePlaidLink } from 'react-plaid-link';
function PlaidLinkButton() {
const [linkToken, setLinkToken] = useState(null);
// 1. Create link token (call your backend)
useEffect(() => {
async function createLinkToken() {
const response = await fetch('/api/plaid/create-link-token', {
method: 'POST',
});
const data = await response.json();
setLinkToken(data.link_token);
}
createLinkToken();
}, []);
// 2. Configure Link
const { open, ready } = usePlaidLink({
token: linkToken,
onSuccess: async (public_token, metadata) => {
// 3. Exchange public token for access token
await fetch('/api/plaid/exchange-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ public_token }),
});
},
onExit: (err, metadata) => {
if (err) console.error(err);
},
});
return (
<button onClick={() => open()} disabled={!ready}>
Connect Bank Account
</button>
);
}
Node.js example:
const plaid = require('plaid');
const client = new plaid.PlaidApi(
new plaid.Configuration({
basePath: plaid.PlaidEnvironments.sandbox,
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
'PLAID-SECRET': process.env.PLAID_SECRET,
},
},
})
);
// Create link token
app.post('/api/plaid/create-link-token', async (req, res) => {
const request = {
user: {
client_user_id: req.user.id, // Your app's user ID
},
client_name: 'Your App Name',
products: ['auth', 'transactions'],
country_codes: ['US'],
language: 'en',
};
try {
const response = await client.linkTokenCreate(request);
res.json({ link_token: response.data.link_token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Store access token securely in your database:
app.post('/api/plaid/exchange-token', async (req, res) => {
const { public_token } = req.body;
try {
const response = await client.itemPublicTokenExchange({
public_token: public_token,
});
const access_token = response.data.access_token;
const item_id = response.data.item_id;
// Store access_token securely in your database
await db.users.update(req.user.id, {
plaid_access_token: access_token,
plaid_item_id: item_id,
});
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
async function getAuthData(access_token) {
const response = await client.authGet({
access_token: access_token,
});
const accounts = response.data.accounts;
const numbers = response.data.numbers;
// ACH numbers
const ach = numbers.ach[0];
console.log('Account:', ach.account);
console.log('Routing:', ach.routing);
return { accounts, numbers };
}
async function getTransactions(access_token) {
const request = {
access_token: access_token,
start_date: '2024-01-01',
end_date: '2024-12-31',
};
const response = await client.transactionsGet(request);
let transactions = response.data.transactions;
// Handle pagination
while (transactions.length < response.data.total_transactions) {
const paginatedRequest = {
...request,
offset: transactions.length,
};
const paginatedResponse = await client.transactionsGet(paginatedRequest);
transactions = transactions.concat(paginatedResponse.data.transactions);
}
return transactions;
}
Transaction object structure:
{
transaction_id: 'abc123',
account_id: 'xyz789',
amount: 12.34, // Positive = money out, Negative = money in
date: '2024-11-16',
name: 'Starbucks',
merchant_name: 'Starbucks',
category: ['Food and Drink', 'Restaurants', 'Coffee Shop'],
pending: false,
payment_channel: 'in store'
}
async function getBalance(access_token) {
const response = await client.accountsBalanceGet({
access_token: access_token,
});
const accounts = response.data.accounts;
accounts.forEach(account => {
console.log(`${account.name}: ${account.balances.current}`);
});
return accounts;
}
async function getIdentity(access_token) {
const response = await client.identityGet({
access_token: access_token,
});
const identity = response.data.accounts[0].owners[0];
console.log('Name:', identity.names[0]);
console.log('Email:', identity.emails[0].data);
console.log('Phone:', identity.phone_numbers[0].data);
return response.data;
}
Configure in Plaid Dashboard or via API when creating link token:
{
webhook: 'https://your-domain.com/api/plaid/webhook',
}
Verify webhook authenticity:
const crypto = require('crypto');
app.post('/api/plaid/webhook', express.json(), async (req, res) => {
const { webhook_type, webhook_code } = req.body;
// Verify webhook signature
const plaidSignature = req.headers['plaid-verification'];
const timestamp = req.headers['plaid-timestamp'];
const payload = JSON.stringify(req.body);
const hash = crypto
.createHmac('sha256', process.env.PLAID_WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest('hex');
if (hash !== plaidSignature) {
return res.status(401).send('Invalid signature');
}
// Handle webhook events
if (webhook_type === 'TRANSACTIONS') {
switch (webhook_code) {
case 'INITIAL_UPDATE':
console.log('Initial transactions available');
break;
case 'DEFAULT_UPDATE':
console.log('New transactions available');
// Fetch new transactions
break;
case 'TRANSACTIONS_REMOVED':
console.log('Transactions removed');
break;
}
}
res.json({ received: true });
});
Common webhook events:
TRANSACTIONS: INITIAL_UPDATE - First batch readyTRANSACTIONS: DEFAULT_UPDATE - New transactions availableITEM: ERROR - Connection issueITEM: PENDING_EXPIRATION - Credentials expiring soonAUTH: AUTOMATICALLY_VERIFIED - Micro-deposit verification completeSandbox (testing):
basePath: plaid.PlaidEnvironments.sandbox
user_good / pass_goodDevelopment (limited live connections):
basePath: plaid.PlaidEnvironments.development
Production (live):
basePath: plaid.PlaidEnvironments.production
When credentials expire or change:
const { open } = usePlaidLink({
token: linkToken,
onSuccess: async (public_token, metadata) => {
// Item re-linked successfully
},
});
// Create link token with update mode
const request = {
access_token: existing_access_token,
// ... other config
};
ITEM_LOGIN_REQUIRED:
RATE_LIMIT_EXCEEDED:
PRODUCT_NOT_READY:
Never expose secret keys client-side:
PLAID_CLIENT_ID and PLAID_SECRET server-side onlyEncrypt access tokens:
Verify webhook signatures:
Use HTTPS:
Implement proper user consent:
Create link token:
// app/api/plaid/create-link-token/route.ts
import { NextResponse } from 'next/server';
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
const client = new PlaidApi(
new Configuration({
basePath: PlaidEnvironments.sandbox,
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID!,
'PLAID-SECRET': process.env.PLAID_SECRET!,
},
},
})
);
export async function POST(req: Request) {
const session = await getSession();
const response = await client.linkTokenCreate({
user: { client_user_id: session.user.id },
client_name: 'Your App',
products: ['auth', 'transactions'],
country_codes: ['US'],
language: 'en',
});
return NextResponse.json({ link_token: response.data.link_token });
}
Test credentials:
user_goodpass_good1234Test institutions:
npm install plaid react-plaid-link