Is Plaid Down? Complete Status Check Guide + Developer Fixes
Plaid Link flow stuck?
Bank connections timing out?
Item logins failing unexpectedly?
Before blaming Plaid, verify if it's actually an outageβor a configuration, institution, or API credential issue. Here's your complete guide to checking Plaid status and resolving common problems fast.
Quick Check: Is Plaid Actually Down?
Don't assume it's Plaid. 70% of "Plaid down" reports are actually:
- Institution-specific outages (bank maintenance)
- Expired or revoked Item logins
- API credential issues (wrong environment keys)
- Rate limiting or quota problems
- Webhook configuration errors
1. Check Official Sources
Plaid Status Page:
π status.plaid.com
What to look for:
- β "All Systems Operational" = Plaid is fine
- β οΈ "Degraded Performance" = Some services affected
- π΄ "Partial Outage" or "Major Outage" = Plaid is down
Real-time updates:
- Link flow status (user authentication)
- Transactions API availability
- Auth/Balance/Identity product status
- Institution-specific issues
- Regional outages (US, Canada, UK, Europe)
Subscribe to updates:
- Email notifications for outages
- Webhook status alerts
- RSS feed for monitoring
Twitter/X Search:
π Search "Plaid down" on Twitter
Why it works:
- Developers report API issues instantly
- See if specific institutions affected
- Plaid support team responds here: @PlaidSupport
Pro tip: If 100+ tweets in the last hour mention "Plaid down" or "Plaid Link broken," it's likely a real outage (not just your integration).
Plaid Developer Community:
π community.plaid.com
Check for:
- Recent outage reports
- Institution-specific issues
- API changes or deprecations
- Known bugs affecting multiple developers
2. Check Service-Specific Status
Plaid has multiple products that can fail independently:
| Product | What It Does | Status Check |
|---|---|---|
| Link | User authentication flow | status.plaid.com |
| Transactions | Transaction history | Check status page under "Products" |
| Auth | Bank account/routing verification | Check status page |
| Balance | Real-time account balances | Check status page |
| Identity | Account holder information | Check status page |
| Investments | Holdings, securities, positions | Check status page |
| Liabilities | Loan, credit card debt data | Check status page |
| Assets | Income verification, assets | Check status page |
| Payment Initiation | Direct bank payments (UK/EU) | Check status page |
Your product might be down while Plaid globally is up.
How to check which product is affected:
- Visit status.plaid.com
- Look for specific product status
- Check "Incident History" for recent issues
- Review "Scheduled Maintenance" (usually Sunday nights)
- Subscribe to product-specific alerts
3. Test Different Environments
If Sandbox works but Production fails, it's likely an institution or credential issue.
| Environment | Purpose | Test Method |
|---|---|---|
| Sandbox | Development/testing | Use Sandbox credentials, test Link flow |
| Development | Pre-production testing | Test with real institutions (limited access) |
| Production | Live user data | Monitor real user connections |
Decision tree:
Sandbox works + Production fails β Institution issue or API key problem
Sandbox fails + Production fails β Plaid system outage
Specific institution fails β Bank maintenance or credentials issue
All institutions fail β Plaid Link infrastructure problem
Common Plaid Error Codes (And What They Mean)
ITEM_LOGIN_REQUIRED
What it means: User needs to re-authenticate via Link.
Causes:
- User changed bank password
- Bank rotated credentials for security
- MFA expired (multi-factor authentication)
- Bank locked account due to suspicious activity
- Item connection revoked by user
How to fix:
- Trigger Link in update mode:
const linkToken = await plaidClient.linkTokenCreate({
user: { client_user_id: userId },
access_token: accessToken, // Pass existing access token
// ... other config
});
- Have user complete Link flow to re-authenticate
- Item status will return to "good" after successful auth
- Resume data fetching
Prevention:
- Set up webhooks for
ITEM_LOGIN_REQUIREDevents - Prompt users proactively when this error occurs
- Implement graceful UI for re-authentication
INSTITUTION_DOWN
What it means: The bank/institution is temporarily unavailable.
Causes:
- Scheduled bank maintenance (common Sunday nights)
- Bank website/API outage
- Bank security updates
- DDoS attacks on bank infrastructure
How to fix:
- Check if institution-specific: status.plaid.com
- Wait and retry with exponential backoff:
// Example retry logic
const retryDelays = [5000, 15000, 60000, 300000]; // 5s, 15s, 1m, 5m
for (const delay of retryDelays) {
await sleep(delay);
try {
const response = await plaidClient.transactionsGet({...});
break; // Success
} catch (err) {
if (err.error_code !== 'INSTITUTION_DOWN') throw err;
}
}
- Show user-friendly message: "Bank maintenance in progress. Trying again in X minutes."
- Set up webhook monitoring for
INSTITUTION_STATUSupdates
Expected duration: 15 minutes to 6 hours (most resolve within 2 hours)
Pro tip: Banks often schedule maintenance Sunday nights 11 PM - 2 AM ET. Avoid high-priority jobs during this window.
INSTITUTION_NOT_RESPONDING
What it means: Bank servers not responding to Plaid's requests.
Similar to: INSTITUTION_DOWN but usually more transient.
Causes:
- Bank server overload
- Network connectivity issues between Plaid and bank
- Bank API rate limiting Plaid
- Temporary bank infrastructure problems
How to fix:
- Retry with exponential backoff (see above)
- Check if widespread: status.plaid.com
- Try alternative products if available (e.g., Balance instead of Transactions)
- Monitor webhooks for status changes
Expected duration: 5 minutes to 1 hour (usually resolves quickly)
INVALID_CREDENTIALS / INVALID_API_KEYS
What it means: Your Plaid API credentials are wrong or expired.
Causes:
- Using wrong environment keys (Sandbox keys in Production)
- Copied keys incorrectly (extra spaces, truncated)
- API keys rotated or regenerated
- Client ID mismatch
How to fix:
- Verify API keys: dashboard.plaid.com/team/keys
- Check environment match:
// Sandbox
const PLAID_CLIENT_ID = 'your_sandbox_client_id';
const PLAID_SECRET = 'your_sandbox_secret';
const PLAID_ENV = 'sandbox';
// Production
const PLAID_CLIENT_ID = 'your_production_client_id';
const PLAID_SECRET = 'your_production_secret';
const PLAID_ENV = 'production';
- Regenerate keys if compromised
- Update environment variables and redeploy
- Test with
/item/getendpoint to verify credentials
Pro tip: Store keys in secure secret management (AWS Secrets Manager, Vault, etc.), not in code.
RATE_LIMIT_EXCEEDED
What it means: You've exceeded Plaid's API rate limits.
Rate limits (as of 2026):
- Sandbox: 1,000 requests/minute (per product)
- Development: 100 requests/minute
- Production: 400 requests/second across all endpoints
Causes:
- Polling Transactions too frequently (use webhooks instead)
- Parallel requests without rate limiting
- Misconfigured retry logic (infinite loops)
- Large batch operations without throttling
How to fix:
- Implement rate limiting in your app:
const Bottleneck = require('bottleneck');
const limiter = new Bottleneck({
maxConcurrent: 20, // Max parallel requests
minTime: 10 // Min 10ms between requests (100 req/sec)
});
const rateLimitedGet = limiter.wrap(plaidClient.transactionsGet);
- Use webhooks instead of polling:
DEFAULT_UPDATEfor TransactionsINITIAL_UPDATEfor first data fetchHISTORICAL_UPDATEfor backfill
- Implement exponential backoff on 429 errors
- Cache responses where appropriate
- Batch operations and spread over time
Expected duration: Immediate (limit resets per minute)
ITEM_NOT_FOUND
What it means: The access_token doesn't exist or was deleted.
Causes:
- Typo in access token
- Item was removed via
item/removeendpoint - User revoked access through bank
- Access token from wrong environment (Sandbox vs Production)
How to fix:
- Verify access token is correct and not truncated
- Check your database for token integrity
- Ensure using correct environment
- If legitimately deleted, prompt user to re-connect via Link
- Handle gracefully in UI (don't show errors to user)
Prevention:
- Store access tokens securely and immutably
- Log all
item/removecalls for debugging - Implement soft deletes in your database
PRODUCTS_NOT_READY
What it means: Data not yet available after initial Link connection.
Causes:
- Transactions still being fetched (initial sync can take 5-30 minutes)
- Historical data backfill in progress
- Institution delay in providing data
- Webhooks not yet fired
How to fix:
- Wait for webhooks before fetching data:
INITIAL_UPDATE(Transactions available)HISTORICAL_UPDATE(Historical data available)DEFAULT_UPDATE(New transactions available)
- Show loading state to users: "Syncing your data... (usually takes 2-5 minutes)"
- Implement polling with timeout:
const maxWaitTime = 300000; // 5 minutes
const pollInterval = 10000; // 10 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
try {
const response = await plaidClient.transactionsGet({...});
return response; // Success
} catch (err) {
if (err.error_code !== 'PRODUCTS_NOT_READY') throw err;
await sleep(pollInterval);
}
}
throw new Error('Timeout waiting for transactions');
- Don't call Transactions API immediately after Link success
Expected duration: 2-10 minutes (average 5 minutes)
ASSET_REPORT_ERROR / ASSET_PRODUCT_NOT_READY
What it means: Asset Report generation failed or still in progress.
Causes:
- Asset Report still generating (can take 1-5 minutes)
- Insufficient transaction history at institution
- Institution doesn't support Assets product
- User denied permission for income verification
How to fix:
- Poll
/asset_report/getwith delays:
const checkAssetReport = async (assetReportToken) => {
const maxRetries = 20;
const retryDelay = 15000; // 15 seconds
for (let i = 0; i < maxRetries; i++) {
try {
const report = await plaidClient.assetReportGet({
asset_report_token: assetReportToken,
});
return report; // Success
} catch (err) {
if (err.error_code !== 'ASSET_PRODUCT_NOT_READY') throw err;
await sleep(retryDelay);
}
}
throw new Error('Asset Report generation timeout');
};
- Set up webhook for
ASSET_REPORT_READY - Show progress indicator to users
- Handle edge case where Assets not supported
Expected duration: 1-5 minutes (average 2 minutes)
PAYMENT_ERROR (Payment Initiation)
What it means: Payment Initiation request failed (UK/EU only).
Causes:
- Insufficient funds in user account
- Payment exceeds bank limits
- Bank requires additional authentication (SCA)
- Payment blocked by bank fraud detection
- Incorrect payment details
How to fix:
- Check error sub-code for specific reason:
INSUFFICIENT_FUNDSβ Ask user to add fundsPAYMENT_LIMIT_EXCEEDEDβ Reduce amount or split paymentSCA_REQUIREDβ Re-authenticate via LinkPAYMENT_REJECTEDβ Contact bank or try different account
- Implement payment status polling:
const payment = await plaidClient.paymentInitiationPaymentGet({
payment_id: paymentId,
});
// Status: INITIATED, PENDING, EXECUTED, REJECTED
- Show user-friendly error messages
- Offer retry or alternative payment methods
Expected duration: Immediate (payment either succeeds or fails)
Quick Fixes: Plaid API Not Working?
Fix #1: Verify API Credentials & Environment
Why it works: 30% of issues are wrong keys or environment mismatch.
How to check:
1. Log into Plaid Dashboard:
π dashboard.plaid.com/team/keys
2. Verify keys match your environment:
// Check your .env or config
console.log('Client ID:', process.env.PLAID_CLIENT_ID);
console.log('Environment:', process.env.PLAID_ENV);
// Don't log PLAID_SECRET in production!
// Verify client initialization
const client = new plaid.PlaidApi(
new plaid.Configuration({
basePath: plaid.PlaidEnvironments[process.env.PLAID_ENV],
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
'PLAID-SECRET': process.env.PLAID_SECRET,
},
},
})
);
3. Test credentials:
curl -X POST https://sandbox.plaid.com/item/get \
-H 'Content-Type: application/json' \
-d '{
"client_id": "your_client_id",
"secret": "your_secret",
"access_token": "access-sandbox-test-token"
}'
Common mistakes:
- Using Sandbox keys in Production (won't work)
- Using Production keys in Development (expensive)
- Extra spaces when copying keys
- Expired or rotated secrets
- Wrong
basePathURL
Fix #2: Check Institution Status
Why it works: Institution outages cause 40% of "Plaid down" reports.
How to check:
1. Visit Plaid Status Page:
π status.plaid.com
2. Search for your institution:
- Click "Institutions" tab
- Search by name (e.g., "Chase", "Bank of America")
- Check if maintenance or outage reported
3. Test specific institution via API:
const response = await plaidClient.institutionsGetById({
institution_id: 'ins_3',
country_codes: ['US'],
});
console.log('Status:', response.data.institution.status);
// Check status.item_logins, status.transactions_updates, etc.
4. Check institution health:
const response = await plaidClient.institutionsGet({
count: 500,
offset: 0,
country_codes: ['US'],
});
// Filter to your institution and check status
const institution = response.data.institutions.find(
inst => inst.institution_id === 'ins_3'
);
console.log('Health status:', institution.status);
Bank maintenance schedule (common):
- Sunday nights: 11 PM - 2 AM ET (most US banks)
- First Saturday of month: 12 AM - 6 AM (some banks)
Pro tip: If user connection fails, check institution status first before debugging your code.
Fix #3: Regenerate Link Token (Link Flow Issues)
Why it works: Link tokens expire after 4 hours and must be regenerated.
Common Link flow issues:
- "Something went wrong" error
- Link modal won't load
- Institution search not working
- White screen after clicking bank
How to fix:
1. Generate fresh Link token:
const createLinkToken = async (userId) => {
try {
const response = await plaidClient.linkTokenCreate({
user: {
client_user_id: userId,
},
client_name: 'Your App Name',
products: ['transactions', 'auth'],
country_codes: ['US'],
language: 'en',
webhook: 'https://your-domain.com/webhooks/plaid',
redirect_uri: 'https://your-app.com/oauth-redirect', // For OAuth institutions
});
return response.data.link_token;
} catch (error) {
console.error('Link token creation failed:', error);
throw error;
}
};
2. Common Link token mistakes:
- Reusing expired token (tokens expire in 4 hours)
- Missing redirect_uri for OAuth institutions (Chase, TD Bank, etc.)
- Wrong webhook URL (must be HTTPS, publicly accessible)
- Missing country_codes (required field)
- Products not enabled (enable in Dashboard β Products)
3. Debug Link with console logging:
Plaid.create({
token: linkToken,
onSuccess: (public_token, metadata) => {
console.log('Success! Public token:', public_token);
console.log('Institution:', metadata.institution);
console.log('Accounts:', metadata.accounts);
// Exchange public_token for access_token
},
onExit: (err, metadata) => {
console.log('Exit:', err, metadata);
if (err) {
console.error('Link error:', err.error_code, err.error_message);
}
},
onEvent: (eventName, metadata) => {
console.log('Event:', eventName, metadata);
},
});
4. Test Link in Sandbox first:
- Use Sandbox credentials
- Test institutions:
ins_109508(Sandbox Test Bank) - Username:
user_good, Password:pass_good
Fix #4: Handle Webhooks Properly
Why it works: Webhooks prevent polling and tell you exactly when data is ready.
Common webhook issues:
- Not receiving webhooks
- Webhooks timing out
- Duplicate webhook processing
- Webhook signature verification failures
How to fix:
1. Verify webhook URL is correct:
// Check Item's webhook
const response = await plaidClient.itemGet({
access_token: accessToken,
});
console.log('Webhook URL:', response.data.item.webhook);
2. Update webhook URL if wrong:
await plaidClient.itemWebhookUpdate({
access_token: accessToken,
webhook: 'https://your-domain.com/webhooks/plaid',
});
3. Test webhook delivery:
- Dashboard β Webhooks β Send Test Webhook
- Check your server logs
- Verify endpoint returns 200 OK within 10 seconds
4. Implement webhook endpoint correctly:
app.post('/webhooks/plaid', async (req, res) => {
const webhook = req.body;
// Respond immediately (don't wait for processing)
res.status(200).send('OK');
// Process asynchronously
try {
switch (webhook.webhook_code) {
case 'DEFAULT_UPDATE':
// New transactions available
await fetchTransactions(webhook.item_id);
break;
case 'INITIAL_UPDATE':
// Initial transactions ready (first time)
await fetchTransactions(webhook.item_id);
break;
case 'HISTORICAL_UPDATE':
// Historical transactions ready
await fetchTransactions(webhook.item_id);
break;
case 'ITEM_LOGIN_REQUIRED':
// User needs to re-authenticate
await notifyUserToReconnect(webhook.item_id);
break;
case 'WEBHOOK_UPDATE_ACKNOWLEDGED':
// Webhook URL updated successfully
console.log('Webhook URL updated');
break;
default:
console.log('Unhandled webhook:', webhook.webhook_code);
}
} catch (error) {
console.error('Webhook processing error:', error);
// Don't throw - we already responded 200
}
});
5. Verify webhook signature (security best practice):
const crypto = require('crypto');
const verifyWebhookSignature = (body, signature) => {
const webhookSecret = process.env.PLAID_WEBHOOK_VERIFICATION_KEY;
const hash = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(body))
.digest('hex');
return hash === signature;
};
app.post('/webhooks/plaid', (req, res) => {
const signature = req.headers['plaid-verification'];
if (!verifyWebhookSignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
res.status(200).send('OK');
});
Webhook requirements:
- Must be HTTPS (not HTTP)
- Must respond with 200 OK within 10 seconds
- Must be publicly accessible (no localhost)
- Should return 200 even if processing fails
Fix #5: Implement Proper Error Handling & Retries
Why it works: Transient errors resolve with smart retry logic.
Retry strategy:
const retryPlaidRequest = async (requestFn, maxRetries = 3) => {
const retryableErrors = [
'INSTITUTION_DOWN',
'INSTITUTION_NOT_RESPONDING',
'INTERNAL_SERVER_ERROR',
'PLANNED_MAINTENANCE',
];
const delays = [1000, 5000, 15000]; // 1s, 5s, 15s
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
const isRetryable = retryableErrors.includes(error.error_code);
const isLastAttempt = attempt === maxRetries - 1;
if (!isRetryable || isLastAttempt) {
throw error; // Don't retry
}
console.log(`Retry ${attempt + 1}/${maxRetries} after ${delays[attempt]}ms`);
await sleep(delays[attempt]);
}
}
};
// Usage
const transactions = await retryPlaidRequest(async () => {
return await plaidClient.transactionsGet({
access_token: accessToken,
start_date: '2026-01-01',
end_date: '2026-02-11',
});
});
Error handling best practices:
const handlePlaidError = (error) => {
const errorCode = error.error_code;
const errorMessage = error.error_message;
switch (errorCode) {
case 'ITEM_LOGIN_REQUIRED':
// Prompt user to re-authenticate
return {
userAction: 'reauth',
message: 'Please reconnect your bank account.',
};
case 'INSTITUTION_DOWN':
// Show maintenance message
return {
userAction: 'wait',
message: 'Your bank is temporarily unavailable. Please try again in 15 minutes.',
};
case 'RATE_LIMIT_EXCEEDED':
// Internal issue, don't expose to user
return {
userAction: 'retry_later',
message: 'Service temporarily busy. Please try again shortly.',
};
case 'INVALID_ACCESS_TOKEN':
case 'ITEM_NOT_FOUND':
// Item deleted or invalid
return {
userAction: 'reconnect',
message: 'Bank connection lost. Please reconnect your account.',
};
default:
console.error('Unhandled Plaid error:', errorCode, errorMessage);
return {
userAction: 'contact_support',
message: 'Something went wrong. Please contact support.',
};
}
};
Fix #6: Use Sandbox for Testing & Development
Why it works: Isolates issues between your code and production data.
Sandbox benefits:
- Instant responses (no bank delays)
- Predictable test accounts
- No rate limits
- Free transactions
- Test all error scenarios
How to use Sandbox:
1. Switch to Sandbox environment:
// .env.development
PLAID_CLIENT_ID=your_sandbox_client_id
PLAID_SECRET=your_sandbox_secret
PLAID_ENV=sandbox
2. Use Sandbox test institutions:
// Sandbox Test Bank (all products)
const SANDBOX_INSTITUTION_ID = 'ins_109508';
// Test credentials
const TEST_ACCOUNTS = {
good: { username: 'user_good', password: 'pass_good' },
locked: { username: 'user_locked', password: 'pass_locked' },
custom: { username: 'user_custom', password: 'pass_custom' },
};
3. Test error scenarios:
// Trigger specific errors in Sandbox
const createSandboxItem = async (scenario) => {
const response = await plaidClient.sandboxPublicTokenCreate({
institution_id: 'ins_109508',
initial_products: ['transactions'],
options: {
webhook: 'https://your-domain.com/webhooks/plaid',
override_username: scenario, // 'user_good', 'user_locked', etc.
},
});
return response.data.public_token;
};
// Test different scenarios
await createSandboxItem('user_good'); // Success
await createSandboxItem('user_locked'); // ITEM_LOGIN_REQUIRED
await createSandboxItem('user_custom'); // Custom MFA
4. Simulate webhooks in Sandbox:
await plaidClient.sandboxItemFireWebhook({
access_token: accessToken,
webhook_code: 'DEFAULT_UPDATE',
});
5. Reset Sandbox Item login:
await plaidClient.sandboxItemResetLogin({
access_token: accessToken,
});
// Triggers ITEM_LOGIN_REQUIRED
Pro tip: Test your entire flow in Sandbox before touching Production. Common issues (webhook handling, error states, Link flow) appear the same way.
Fix #7: Monitor Plaid API Requests & Responses
Why it works: Logging reveals patterns in failures (specific institutions, times, error codes).
Implement request logging:
const logPlaidRequest = (endpoint, request, response, error) => {
const log = {
timestamp: new Date().toISOString(),
endpoint,
request: {
// Don't log secrets
client_id: request.client_id,
access_token: request.access_token ? '[REDACTED]' : undefined,
},
response: response ? {
status: 'success',
data: response.data,
} : undefined,
error: error ? {
error_code: error.error_code,
error_message: error.error_message,
error_type: error.error_type,
display_message: error.display_message,
} : undefined,
};
console.log(JSON.stringify(log));
// Send to monitoring (Datadog, Sentry, etc.)
if (error) {
Sentry.captureException(error, {
tags: {
plaid_endpoint: endpoint,
plaid_error_code: error.error_code,
},
});
}
};
// Wrap Plaid calls
const transactionsGet = async (accessToken, startDate, endDate) => {
const request = { access_token: accessToken, start_date: startDate, end_date: endDate };
try {
const response = await plaidClient.transactionsGet(request);
logPlaidRequest('/transactions/get', request, response, null);
return response;
} catch (error) {
logPlaidRequest('/transactions/get', request, null, error);
throw error;
}
};
Monitoring dashboards (recommended):
- Datadog: Track error rates by error_code
- Sentry: Alert on new error types
- CloudWatch: Graph request volume and latency
- Custom dashboard: Institution success rates
Key metrics to track:
- Error rate by error_code
- Average response time per endpoint
- Institution success rate (login success %)
- Webhook delivery success rate
- Items requiring re-authentication (ITEM_LOGIN_REQUIRED)
Plaid Link Issues
Issue: Link Modal Won't Load
Symptoms:
- White screen after clicking bank
- "Something went wrong" error
- Link spinner indefinitely
- Institution search not working
Troubleshoot:
1. Check Link token:
// Token must be fresh (< 4 hours old)
const tokenCreatedAt = Date.now();
const tokenAge = Date.now() - tokenCreatedAt;
const isExpired = tokenAge > (4 * 60 * 60 * 1000); // 4 hours
if (isExpired) {
console.error('Link token expired, generating new one');
linkToken = await createLinkToken(userId);
}
2. Check browser console:
- Open DevTools (F12)
- Look for JavaScript errors
- Check Network tab for failed requests
- Look for CORS errors (webhook/redirect_uri issues)
3. Verify Link initialization:
const linkHandler = Plaid.create({
token: linkToken,
onLoad: () => {
console.log('Link loaded successfully');
},
onSuccess: (public_token, metadata) => {
console.log('Link success:', public_token);
},
onExit: (err, metadata) => {
if (err) {
console.error('Link error:', err);
}
},
onEvent: (eventName, metadata) => {
console.log('Link event:', eventName, metadata);
},
});
linkHandler.open();
4. Common Link issues:
- Ad blockers blocking Plaid CDN
- Privacy extensions blocking third-party scripts
- Corporate firewalls blocking cdn.plaid.com
- Content Security Policy too restrictive
- Popup blockers preventing Link modal
5. Test in incognito mode:
- Disables most extensions
- Isolates browser-specific issues
Issue: OAuth Redirect Failing (Chase, TD Bank, etc.)
Symptoms:
- Redirected to bank website
- After login, stuck or returns to error page
- "redirect_uri mismatch" error
Causes:
redirect_urinot configured in Link token- Redirect URI not whitelisted in Plaid Dashboard
- HTTPS required (HTTP won't work)
- Redirect URI must match exactly (including trailing slash)
How to fix:
1. Configure redirect_uri in Link token:
const response = await plaidClient.linkTokenCreate({
user: { client_user_id: userId },
client_name: 'Your App',
products: ['transactions'],
country_codes: ['US'],
language: 'en',
redirect_uri: 'https://your-app.com/oauth-redirect', // Required for OAuth
});
2. Whitelist redirect URI in Dashboard:
- Login to dashboard.plaid.com
- Team Settings β API β Allowed redirect URIs
- Add your redirect URI:
https://your-app.com/oauth-redirect - Must match exactly (case-sensitive, trailing slash matters)
3. Implement redirect handler:
// /oauth-redirect page
const urlParams = new URLSearchParams(window.location.search);
const oauthStateId = urlParams.get('oauth_state_id');
if (oauthStateId) {
// Continue Link flow
Plaid.create({
token: linkToken,
receivedRedirectUri: window.location.href,
}).open();
} else {
console.error('Missing oauth_state_id');
}
OAuth institutions (require redirect_uri):
- Chase
- TD Bank
- BB&T / Truist
- Capital One 360
- HSBC
- Citibank
Issue: User Stuck on MFA (Multi-Factor Authentication)
Symptoms:
- Link asks for security code
- Code sent to phone/email
- User enters code but Link doesn't proceed
Causes:
- Institution delay sending code (30s - 2min)
- User entering wrong code
- Code expired (usually 5-10 min expiry)
- Institution MFA system issues
How to fix:
1. Wait for code delivery:
- SMS codes: 30 seconds to 2 minutes
- Email codes: 30 seconds to 5 minutes
- Voice call: 1-2 minutes
2. Resend code option:
- Most institutions allow "Resend code" after 60 seconds
- Click "Resend" in Link flow
3. Try different MFA method:
- If SMS fails, try email or voice call
- Some institutions offer app-based MFA (faster)
4. Check institution status:
- MFA issues often correlate with institution outages
- Check status.plaid.com
5. Let user retry login:
- Link allows going back to credential entry
- User can re-enter username/password
Pro tip: Educate users about MFA in onboarding: "Your bank may send you a security code. This is normal."
Plaid Transactions Issues
Issue: Missing or Incomplete Transactions
Symptoms:
- Transaction count lower than expected
- Recent transactions not appearing
- Historical transactions missing
- Gaps in transaction history
Causes:
- Still fetching initial transactions (wait for webhook)
- Institution provides limited history (30-90 days typical)
- Pending transactions not included (depending on settings)
- Account type not supported (some accounts excluded)
How to fix:
1. Wait for INITIAL_UPDATE webhook:
// After Link success, wait for webhook before fetching
app.post('/webhooks/plaid', (req, res) => {
if (req.body.webhook_code === 'INITIAL_UPDATE') {
// Now safe to fetch transactions
fetchTransactions(req.body.item_id);
}
res.status(200).send('OK');
});
2. Check transaction date range:
// Plaid returns up to 24 months of history
const response = await plaidClient.transactionsGet({
access_token: accessToken,
start_date: '2024-01-01', // Up to 2 years ago
end_date: '2026-02-11',
options: {
include_personal_finance_category: true,
},
});
console.log('Total transactions:', response.data.total_transactions);
console.log('Returned:', response.data.transactions.length);
3. Handle pagination:
const getAllTransactions = async (accessToken, startDate, endDate) => {
let allTransactions = [];
let hasMore = true;
let offset = 0;
while (hasMore) {
const response = await plaidClient.transactionsGet({
access_token: accessToken,
start_date: startDate,
end_date: endDate,
options: {
offset: offset,
count: 500, // Max per request
},
});
allTransactions = allTransactions.concat(response.data.transactions);
offset += response.data.transactions.length;
hasMore = response.data.transactions.length === 500;
}
return allTransactions;
};
4. Check if pending transactions included:
// Pending transactions are included by default
// To exclude pending:
const response = await plaidClient.transactionsGet({
access_token: accessToken,
start_date: startDate,
end_date: endDate,
options: {
include_pending: false, // Exclude pending
},
});
5. Verify institution limitations:
- Some banks only provide 30-90 days of history
- Credit card accounts may have different limits than checking
- Investment accounts may not support Transactions product
Issue: Duplicate Transactions
Symptoms:
- Same transaction appearing multiple times
- Different
transaction_idbut same details - Duplicate charges from merchant
Causes:
- Bank correcting/updating transaction (expected behavior)
- Pending transaction posted (creates new transaction_id)
- Multiple webhook deliveries processed
- Institution data quality issues
How to fix:
1. Use Plaid's transaction_id as unique identifier:
// Store transactions with transaction_id as primary key
const upsertTransaction = async (transaction) => {
await db.query(
`INSERT INTO transactions (transaction_id, account_id, amount, date, name)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (transaction_id) DO UPDATE SET
amount = EXCLUDED.amount,
date = EXCLUDED.date,
name = EXCLUDED.name`,
[transaction.transaction_id, transaction.account_id, transaction.amount,
transaction.date, transaction.name]
);
};
2. Handle removed transactions (from TRANSACTIONS_REMOVED webhook):
app.post('/webhooks/plaid', async (req, res) => {
const webhook = req.body;
if (webhook.webhook_code === 'TRANSACTIONS_REMOVED') {
// Remove transactions that were corrected/reversed
const removedIds = webhook.removed_transactions;
await db.query(
'DELETE FROM transactions WHERE transaction_id = ANY($1)',
[removedIds]
);
}
res.status(200).send('OK');
});
3. Deduplicate by merchant name + amount + date:
// Secondary deduplication (user-facing)
const deduplicateTransactions = (transactions) => {
const seen = new Set();
return transactions.filter(txn => {
const key = `${txn.name}_${txn.amount}_${txn.date}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
};
4. Monitor pending field:
// Track pending β posted transitions
transactions.forEach(txn => {
if (txn.pending) {
console.log('Pending transaction:', txn.transaction_id);
// Show as "pending" in UI
} else {
console.log('Posted transaction:', txn.transaction_id);
}
});
Issue: Transaction Categorization Wrong
Symptoms:
- Grocery store categorized as "Travel"
- Rent payment categorized as "Transfer"
- Business expense showing as personal
Causes:
- Plaid's ML categorization not perfect
- Merchant name ambiguous
- Internal transfers hard to categorize
- User's use case differs from typical
How to fix:
1. Use Personal Finance Categories (PFC):
const response = await plaidClient.transactionsGet({
access_token: accessToken,
start_date: startDate,
end_date: endDate,
options: {
include_personal_finance_category: true,
},
});
transactions.forEach(txn => {
const category = txn.personal_finance_category;
console.log('Primary:', category.primary); // e.g., "FOOD_AND_DRINK"
console.log('Detailed:', category.detailed); // e.g., "FOOD_AND_DRINK_GROCERIES"
console.log('Confidence:', category.confidence_level); // "HIGH", "MEDIUM", "LOW"
});
2. Allow user overrides:
// Let users recategorize transactions
const updateTransactionCategory = async (transactionId, newCategory) => {
await db.query(
'UPDATE transactions SET user_category = $1 WHERE transaction_id = $2',
[newCategory, transactionId]
);
// Learn from user's override (ML model improvement)
// Store for future similar transactions
};
3. Use merchant name patterns:
const improveCategories = (transactions) => {
return transactions.map(txn => {
let category = txn.personal_finance_category?.detailed;
// Custom rules
if (txn.name.includes('RENT') || txn.name.includes('LANDLORD')) {
category = 'RENT_AND_UTILITIES_RENT';
} else if (txn.name.includes('VENMO') || txn.name.includes('PAYPAL')) {
category = 'TRANSFER_OUT_PEER_TO_PEER';
}
return { ...txn, improved_category: category };
});
};
4. Filter internal transfers:
// Exclude transfers between user's own accounts
const response = await plaidClient.transactionsGet({
access_token: accessToken,
start_date: startDate,
end_date: endDate,
options: {
include_personal_finance_category: true,
},
});
const externalTransactions = response.data.transactions.filter(txn => {
const category = txn.personal_finance_category?.primary;
return category !== 'TRANSFER_IN' && category !== 'TRANSFER_OUT';
});
Plaid Auth & Balance Issues
Issue: Balance Not Updating
Symptoms:
- Balance shows old/stale value
- Real-time balance doesn't match bank
- Balance only updates once per day
Causes:
- Institution delay (some banks update once daily)
- Using Transactions balance instead of Balance product
- Webhook not triggering balance refresh
- Cached balance data
How to fix:
1. Use Balance product for real-time balances:
// Balance product (real-time)
const response = await plaidClient.accountsBalanceGet({
access_token: accessToken,
});
response.data.accounts.forEach(account => {
console.log('Account:', account.name);
console.log('Current balance:', account.balances.current);
console.log('Available balance:', account.balances.available);
console.log('Limit:', account.balances.limit); // Credit cards
console.log('Last updated:', account.balances.last_updated_datetime);
});
2. Understand balance types:
current: Current balance (includes pending)available: Available for spending (excludes pending/holds)limit: Credit limit (credit cards only)
3. Poll balance before displaying:
// Fetch fresh balance on user request
const getFreshBalance = async (accessToken) => {
const response = await plaidClient.accountsBalanceGet({
access_token: accessToken,
});
return response.data.accounts;
};
4. Check institution update frequency:
- Most banks: Real-time or hourly
- Some smaller banks: Once daily
- Credit unions: Often daily only
Issue: Account/Routing Number Wrong (Auth Product)
Symptoms:
- Account number doesn't match bank statement
- Routing number incorrect
- ACH payments failing
Causes:
- Tokenized account numbers (privacy feature)
- Wire routing vs ACH routing different
- Account number format varies by institution
- Wrong account selected
How to fix:
1. Use Auth product correctly:
const response = await plaidClient.authGet({
access_token: accessToken,
});
response.data.numbers.ach.forEach(account => {
console.log('Account ID:', account.account_id);
console.log('Account number:', account.account);
console.log('Routing number:', account.routing);
console.log('Wire routing:', account.wire_routing); // May differ
});
2. Verify account type supports ACH:
- Checking: β Supported
- Savings: β Supported
- Credit cards: β Not supported
- Investment: β οΈ Limited support
3. Use correct routing for transfer type:
const getRoutingNumber = (account, transferType) => {
if (transferType === 'wire') {
return account.wire_routing || account.routing;
} else {
return account.routing; // ACH
}
};
When Plaid Actually Goes Down
What Happens
Recent major outages:
- November 2025: 4-hour partial outage (Link authentication issues)
- July 2025: 2-hour Transactions API degradation
- March 2025: 1-hour webhook delivery delays
- December 2024: 3-hour Chase-specific outage
Typical causes:
- AWS/cloud provider outages
- Institution-side API changes
- DDoS attacks (rare)
- Deployment bugs
- Database issues
- Third-party dependency failures
How Plaid Responds
Communication channels:
- status.plaid.com - Primary source (updated every 15-30 min)
- @PlaidSupport on Twitter/X
- Email alerts (if subscribed to status page)
- Webhook status updates (for critical failures)
- Dashboard notifications
Timeline:
- 0-15 min: Developers report issues on Twitter
- 15-30 min: Plaid acknowledges on status page ("Investigating")
- 30-90 min: Root cause identified ("Identified")
- 1-4 hours: Fix deployed ("Monitoring")
- Resolution: Post-mortem published within 48 hours
What to Do During Outages
1. Check scope:
- All products affected, or just one?
- All institutions, or specific banks?
- All regions, or US-only?
2. Switch to fallback strategy:
// Graceful degradation
const getTransactions = async (accessToken) => {
try {
return await plaidClient.transactionsGet({...});
} catch (error) {
if (error.error_code === 'INTERNAL_SERVER_ERROR') {
// Plaid down, use cached data
return await db.getCachedTransactions(accessToken);
}
throw error;
}
};
3. Communicate with users:
// Show user-friendly message
if (plaidIsDown) {
return {
message: "We're experiencing issues connecting to your bank. Your data is safe and we're working to restore service. Please check back in 30 minutes.",
showCachedData: true,
retryAfter: Date.now() + (30 * 60 * 1000),
};
}
4. Queue failed requests:
// Retry automatically when service restored
const queuedRequests = new Queue();
const fetchWithQueue = async (accessToken) => {
try {
return await plaidClient.transactionsGet({...});
} catch (error) {
if (isPlaidOutage(error)) {
queuedRequests.add({ accessToken, retryAfter: Date.now() + 300000 });
throw new Error('Queued for retry');
}
throw error;
}
};
5. Monitor status page:
- Subscribe to status.plaid.com updates
- Set up API Status Check monitoring
- Get alerts via Slack/Discord/email when service restored
Plaid Down Checklist
Follow these steps in order:
Step 1: Verify it's actually down
- Check Plaid Status
- Check API Status Check
- Search Twitter: "Plaid down" or "Plaid API"
- Test in Sandbox (isolate production vs Plaid issue)
- Check specific institution status
Step 2: Verify credentials & configuration
- API credentials correct (client_id, secret, environment)
- Using correct environment (Sandbox vs Production)
- Webhook URL accessible and returning 200 OK
- Products enabled in Dashboard
- Rate limits not exceeded
Step 3: Debug specific error
- Log full error response (error_code, error_message)
- Check error code documentation
- Implement appropriate retry logic
- Verify access_token is valid and not expired
- Check Item status with
/item/get
Step 4: Test in isolation
- Test with Sandbox credentials
- Test with different institution
- Test different API endpoint (e.g., Balance instead of Transactions)
- Test from different network (mobile hotspot)
- Test with freshly generated Link token
Step 5: Link-specific debugging
- Generate fresh Link token (< 4 hours old)
- Check browser console for errors
- Test in incognito mode (disable extensions)
- Verify redirect_uri configured (for OAuth institutions)
- Test with Sandbox institution first
Step 6: Contact support
- Gather logs (request_id, error_code, timestamp)
- Document steps to reproduce
- Include environment (Sandbox vs Production)
- Submit ticket: dashboard.plaid.com/support
- Post in community: community.plaid.com
Prevent Future Issues
1. Implement Robust Webhook Handling
Why it matters: Webhooks prevent polling and catch issues early.
Best practices:
1. Set up webhook endpoint:
app.post('/webhooks/plaid', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processWebhook(req.body).catch(err => {
console.error('Webhook processing error:', err);
});
});
const processWebhook = async (webhook) => {
const { webhook_code, item_id, error } = webhook;
switch (webhook_code) {
case 'DEFAULT_UPDATE':
await syncTransactions(item_id);
break;
case 'ITEM_LOGIN_REQUIRED':
await notifyUserReauth(item_id);
break;
case 'ERROR':
await handleItemError(item_id, error);
break;
}
};
2. Monitor webhook delivery:
// Track webhook metrics
const webhookMetrics = {
received: 0,
processed: 0,
failed: 0,
averageProcessingTime: 0,
};
// Alert if delivery drops
if (webhookMetrics.failed / webhookMetrics.received > 0.05) {
alertOps('High webhook failure rate');
}
2. Monitor Item Health
Why it matters: Catch authentication issues before users complain.
Health check script:
const checkItemHealth = async () => {
const items = await db.getAllItems();
const unhealthyItems = [];
for (const item of items) {
try {
const response = await plaidClient.itemGet({
access_token: item.access_token,
});
const status = response.data.status;
if (status.item_logins.status !== 'HEALTHY') {
unhealthyItems.push({
item_id: item.item_id,
user_id: item.user_id,
status: status.item_logins.status,
last_successful_update: status.item_logins.last_successful_update,
});
}
} catch (error) {
unhealthyItems.push({
item_id: item.item_id,
user_id: item.user_id,
error: error.error_code,
});
}
}
// Notify users or admins
if (unhealthyItems.length > 0) {
console.log('Unhealthy items:', unhealthyItems.length);
await notifyUsersToReconnect(unhealthyItems);
}
};
// Run daily
schedule.daily('02:00', checkItemHealth);
3. Cache Data Strategically
Why it matters: Graceful degradation during outages or rate limits.
Caching strategy:
const getCachedTransactions = async (accessToken, startDate, endDate) => {
const cacheKey = `txns:${accessToken}:${startDate}:${endDate}`;
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
const data = JSON.parse(cached);
const age = Date.now() - data.timestamp;
// Cache valid for 1 hour
if (age < 3600000) {
return { data: data.transactions, cached: true };
}
}
// Fetch fresh data
try {
const response = await plaidClient.transactionsGet({
access_token: accessToken,
start_date: startDate,
end_date: endDate,
});
// Update cache
await redis.set(
cacheKey,
JSON.stringify({
transactions: response.data.transactions,
timestamp: Date.now(),
}),
'EX',
3600 // 1 hour expiry
);
return { data: response.data.transactions, cached: false };
} catch (error) {
// If Plaid down, return stale cache
if (cached) {
return { data: JSON.parse(cached).transactions, cached: true, stale: true };
}
throw error;
}
};
4. Set Up Monitoring & Alerts
Why it matters: Detect issues before users do.
Monitoring checklist:
1. API response times:
const monitorAPILatency = async (endpoint, requestFn) => {
const start = Date.now();
try {
const response = await requestFn();
const latency = Date.now() - start;
// Alert if slow
if (latency > 5000) {
alertOps(`Slow Plaid response: ${endpoint} took ${latency}ms`);
}
return response;
} catch (error) {
const latency = Date.now() - start;
console.error(`${endpoint} failed after ${latency}ms:`, error.error_code);
throw error;
}
};
2. Error rate tracking:
// Track error rates by code
const errorCounts = new Map();
const trackError = (errorCode) => {
const count = errorCounts.get(errorCode) || 0;
errorCounts.set(errorCode, count + 1);
// Alert if spike
if (count > 10 && count % 10 === 0) {
alertOps(`Spike in ${errorCode} errors: ${count} occurrences`);
}
};
3. Use API Status Check for proactive alerts:
- apistatuscheck.com/api/plaid
- Set up Slack/Discord alerts
- Monitor historical uptime
- Get notified of outages before users complain
5. Document Runbooks
Why it matters: Faster incident response during outages.
Incident runbook template:
# Plaid Outage Runbook
## 1. Detection
- Alert fired: Plaid API error rate > 10%
- Check status.plaid.com
- Check @PlaidSupport Twitter
- Verify in Sandbox (isolate issue)
## 2. Immediate Actions
- Enable cached data fallback
- Update status page: "Investigating bank sync issues"
- Queue failed requests for retry
- Notify team in Slack
## 3. User Communication
- If downtime > 15 min: Email users
- Show banner: "Bank sync temporarily unavailable"
- Provide ETA if Plaid status page has one
## 4. Monitoring
- Track error rates every 5 minutes
- Monitor Plaid status page for updates
- Check when errors drop below 5%
## 5. Recovery
- Re-enable live data fetching
- Process queued requests
- Sync missed webhooks
- Update status page: "All systems operational"
- Post-mortem: Document root cause and improvements
## 6. Contacts
- Plaid Support: support@plaid.com
- Plaid Dashboard: dashboard.plaid.com/support
- On-call engineer: [phone number]
Key Takeaways
Before assuming Plaid is down:
- β Check Plaid Status
- β Test in Sandbox (isolate production issues)
- β Verify API credentials and environment
- β Check specific institution status
- β Search Twitter for "Plaid down"
Common fixes:
- Regenerate Link token (expires after 4 hours)
- Verify webhook URL is accessible
- Implement proper retry logic for transient errors
- Use webhooks instead of polling (prevents rate limits)
- Check institution status (not Plaid-wide outage)
Error handling best practices:
- Log all API calls (request_id, error_code, timestamp)
- Retry transient errors (
INSTITUTION_DOWN, etc.) - Prompt users for re-auth on
ITEM_LOGIN_REQUIRED - Cache data for graceful degradation
- Monitor error rates and alert on spikes
If Plaid is actually down:
- Monitor status.plaid.com
- Show cached data to users
- Queue failed requests for retry
- Communicate expected downtime
- Usually resolved within 1-4 hours
Prevent future issues:
- Set up robust webhook handling
- Monitor Item health daily
- Implement caching strategy
- Use API Status Check for proactive alerts
- Test thoroughly in Sandbox before production
- Document runbooks for incidents
Remember: Most "Plaid down" issues are actually:
- Institution-specific outages (check status.plaid.com)
- Authentication expired (
ITEM_LOGIN_REQUIRED) - Wrong API credentials or environment
- Rate limiting or configuration errors
Try the debugging steps in this guide before assuming Plaid is down.
Need real-time Plaid status monitoring? Track Plaid uptime with API Status Check - Get instant alerts when Plaid goes down or when specific institutions have issues.
Related Resources
- Is Plaid Down Right Now? β Live status check
- Plaid Outage History β Past incidents and downtime analytics
- Plaid vs Teller vs MX Comparison β Which provider is most reliable?
- Financial API Integration Guide β Best practices for Plaid implementation
- API Outage Response Plan β How to handle third-party API downtime
π Tools We Recommend
Uptime monitoring, incident management, and status pages β know before your users do.
Securely manage API keys, database credentials, and service tokens across your team.
Remove your personal data from 350+ data broker sites automatically.
Monitor your developer content performance and track API documentation rankings.
API Status Check
Stop checking API status pages manually
Get instant email alerts when OpenAI, Stripe, AWS, and 100+ APIs go down. Know before your users do.
Free dashboard available Β· 14-day trial on paid plans Β· Cancel anytime
Browse Free Dashboard β