| name | plaid-expert |
| description | Expert guidance for Plaid banking API integration — Link flow, Auth (routing/account numbers for ACH), Transactions (sync/categorize), Identity (KYC/account holder), Accounts (balance/types), and webhook handling. Invoke when user mentions Plaid, bank connections, ACH verification, account ownership, transaction history, KYC, or Plaid webhooks. Example queries — "connect a bank account with Plaid", "retrieve ACH routing numbers", "sync transactions since last webhook", "verify user identity via Plaid". |
| allowed-tools | Read, Write, Edit, Grep, Glob, Bash, WebFetch |
| model | sonnet |
Plaid Integration Expert
Purpose
Provide comprehensive guidance for integrating Plaid's financial data APIs to connect bank accounts, retrieve transactions, verify identities, and process ACH transfers.
When to Use
Invoke when user mentions:
- "Plaid" or "banking API"
- "Link" (Plaid Link flow)
- "bank account connection" or "financial data"
- "ACH transfers" or "bank-to-bank payments"
- "transaction history" or "account balance"
- "income verification" or "identity verification"
Core Products
1. Auth
Purpose: Retrieve bank account and routing numbers for ACH, wire transfers, and bank-to-bank payments.
Use cases:
- ACH payment processing
- Wire transfers
- Bank account verification
2. Transactions
Purpose: Access transaction history for budgeting, expense tracking, and financial insights.
Features:
- Up to 24 months of history
- Categorized transactions
- Merchant information
- Pending and posted transactions
3. Identity
Purpose: Verify user identity through bank account ownership.
Data retrieved:
- Account holder names
- Email addresses
- Phone numbers
- Physical addresses
4. Balance
Purpose: Real-time account balance checking to prevent payment failures.
Use cases:
- Insufficient funds detection
- Payment preflight checks
- Account monitoring
5. Investments
Purpose: Access holdings and transactions from investment accounts.
Coverage:
- Stocks, bonds, ETFs
- 401(k), IRA, brokerage accounts
- Real-time valuations
6. Liabilities
Purpose: Loan and credit data access.
Coverage:
- Student loans
- Mortgages
- Credit cards
- Auto loans
Plaid Link Integration
What is Link?
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.
Link Flow
- User clicks "Connect Bank Account"
- Link modal opens
- User searches for their bank
- User enters credentials (or OAuth)
- User selects accounts to link
- Link returns a
public_token
- Exchange
public_token for access_token server-side
Frontend Integration (React)
Install:
npm install react-plaid-link
Implementation:
import { usePlaidLink } from 'react-plaid-link';
function PlaidLinkButton() {
const [linkToken, setLinkToken] = useState(null);
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();
}, []);
const { open, ready } = usePlaidLink({
token: linkToken,
onSuccess: async (public_token, metadata) => {
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>
);
}
Backend: Create Link Token
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,
},
},
})
);
app.post('/api/plaid/create-link-token', async (req, res) => {
const request = {
user: {
client_user_id: req.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 });
}
});
Backend: Exchange Public Token
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;
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 });
}
});
Retrieving Data
Get Auth (Account/Routing Numbers)
async function getAuthData(access_token) {
const response = await client.authGet({
access_token: access_token,
});
const accounts = response.data.accounts;
const numbers = response.data.numbers;
const ach = numbers.ach[0];
console.log('Account:', ach.account);
console.log('Routing:', ach.routing);
return { accounts, numbers };
}
Get Transactions
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;
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,
date: '2024-11-16',
name: 'Starbucks',
merchant_name: 'Starbucks',
category: ['Food and Drink', 'Restaurants', 'Coffee Shop'],
pending: false,
payment_channel: 'in store'
}
Get Balance
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;
}
Get Identity
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;
}
Webhooks
Setup Webhook URL
Configure in Plaid Dashboard or via API when creating link token:
{
webhook: 'https://your-domain.com/api/plaid/webhook',
}
Webhook Verification
Verify webhook authenticity:
const crypto = require('crypto');
app.post('/api/plaid/webhook', express.json(), async (req, res) => {
const { webhook_type, webhook_code } = req.body;
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');
}
if (webhook_type === 'TRANSACTIONS') {
switch (webhook_code) {
case 'INITIAL_UPDATE':
console.log('Initial transactions available');
break;
case 'DEFAULT_UPDATE':
console.log('New transactions available');
break;
case 'TRANSACTIONS_REMOVED':
console.log('Transactions removed');
break;
}
}
res.json({ received: true });
});
Common webhook events:
TRANSACTIONS: INITIAL_UPDATE - First batch ready
TRANSACTIONS: DEFAULT_UPDATE - New transactions available
ITEM: ERROR - Connection issue
ITEM: PENDING_EXPIRATION - Credentials expiring soon
AUTH: AUTOMATICALLY_VERIFIED - Micro-deposit verification complete
Environments
Sandbox (testing):
basePath: plaid.PlaidEnvironments.sandbox
- Test credentials:
user_good / pass_good
- No real bank connections
- Simulate transactions
Development (limited live connections):
basePath: plaid.PlaidEnvironments.development
- Up to 100 live bank connections
- Free for testing
Production (live):
basePath: plaid.PlaidEnvironments.production
- Real bank connections
- Requires approval and billing
Error Handling
Update Mode (Re-authentication)
When credentials expire or change:
const { open } = usePlaidLink({
token: linkToken,
onSuccess: async (public_token, metadata) => {
},
});
const request = {
access_token: existing_access_token,
};
Common Errors
ITEM_LOGIN_REQUIRED:
- User needs to re-authenticate
- Solution: Trigger Link in update mode
RATE_LIMIT_EXCEEDED:
- Too many requests
- Solution: Implement exponential backoff
PRODUCT_NOT_READY:
- Data still syncing
- Solution: Wait for webhook or retry
Security Best Practices
-
Never expose secret keys client-side:
- Use
PLAID_CLIENT_ID and PLAID_SECRET server-side only
- Link tokens are safe for client use
-
Encrypt access tokens:
- Store access tokens encrypted in database
- Never log access tokens
-
Verify webhook signatures:
- Prevents forged webhooks
- Use crypto verification
-
Use HTTPS:
- Required for webhook endpoints
- Required for production
-
Implement proper user consent:
- Clear disclosure about data access
- Follow Plaid's branding guidelines
Next.js API Route Example
Create link token:
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 });
}
Testing in Sandbox
Test credentials:
- Username:
user_good
- Password:
pass_good
- MFA:
1234
Test institutions:
- "Platypus Bank" - Full feature support
- "Tartan Bank" - OAuth flow
- "Houndstooth Bank" - Error testing
Useful Resources
Implementation Checklist
Product API Reference
Auth — ACH routing and account numbers
const response = await client.authGet({ access_token });
const { accounts, numbers } = response.data;
Verification methods, in order of preference:
- Instant verification (preferred) — credentials flow via Link, Auth returns numbers immediately.
- Same-day micro-deposits — falls back when instant is unavailable.
- Manual micro-deposits — 1–2 business days.
Transactions — sync pattern (preferred over /transactions/get)
let cursor = loadCursor(itemId);
let hasMore = true;
while (hasMore) {
const res = await client.transactionsSync({ access_token, cursor });
saveTransactions(res.data.added, res.data.modified, res.data.removed);
cursor = res.data.next_cursor;
hasMore = res.data.has_more;
}
saveCursor(itemId, cursor);
Webhook: listen for SYNC_UPDATES_AVAILABLE → call transactionsSync with stored cursor. Do not use deprecated TRANSACTIONS_INITIAL_UPDATE/HISTORICAL_UPDATE for new integrations.
Identity — KYC data from the bank
const response = await client.identityGet({ access_token });
for (const account of response.data.accounts) {
for (const owner of account.owners) {
}
}
Use for KYC, fraud prevention (compare bank-reported name vs. user-entered). Not a replacement for full KYC providers — Plaid Identity returns what the bank has on file, which may be stale.
Accounts — balance and account types
const balRes = await client.accountsBalanceGet({ access_token });
const accRes = await client.accountsGet({ access_token });
Preflight an ACH debit by calling accountsBalanceGet first; compare balances.available vs. the debit amount to avoid NSF. Handle PRODUCT_NOT_READY on first call after Link (retry with backoff).