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:

  1. Visit status.plaid.com
  2. Look for specific product status
  3. Check "Incident History" for recent issues
  4. Review "Scheduled Maintenance" (usually Sunday nights)
  5. 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:

  1. Trigger Link in update mode:
const linkToken = await plaidClient.linkTokenCreate({
  user: { client_user_id: userId },
  access_token: accessToken, // Pass existing access token
  // ... other config
});
  1. Have user complete Link flow to re-authenticate
  2. Item status will return to "good" after successful auth
  3. Resume data fetching

Prevention:

  • Set up webhooks for ITEM_LOGIN_REQUIRED events
  • 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:

  1. Check if institution-specific: status.plaid.com
  2. 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;
  }
}
  1. Show user-friendly message: "Bank maintenance in progress. Trying again in X minutes."
  2. Set up webhook monitoring for INSTITUTION_STATUS updates

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:

  1. Retry with exponential backoff (see above)
  2. Check if widespread: status.plaid.com
  3. Try alternative products if available (e.g., Balance instead of Transactions)
  4. 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:

  1. Verify API keys: dashboard.plaid.com/team/keys
  2. 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';
  1. Regenerate keys if compromised
  2. Update environment variables and redeploy
  3. Test with /item/get endpoint 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:

  1. 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);
  1. Use webhooks instead of polling:
    • DEFAULT_UPDATE for Transactions
    • INITIAL_UPDATE for first data fetch
    • HISTORICAL_UPDATE for backfill
  2. Implement exponential backoff on 429 errors
  3. Cache responses where appropriate
  4. 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/remove endpoint
  • User revoked access through bank
  • Access token from wrong environment (Sandbox vs Production)

How to fix:

  1. Verify access token is correct and not truncated
  2. Check your database for token integrity
  3. Ensure using correct environment
  4. If legitimately deleted, prompt user to re-connect via Link
  5. Handle gracefully in UI (don't show errors to user)

Prevention:

  • Store access tokens securely and immutably
  • Log all item/remove calls 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:

  1. Wait for webhooks before fetching data:
    • INITIAL_UPDATE (Transactions available)
    • HISTORICAL_UPDATE (Historical data available)
    • DEFAULT_UPDATE (New transactions available)
  2. Show loading state to users: "Syncing your data... (usually takes 2-5 minutes)"
  3. 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');
  1. 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:

  1. Poll /asset_report/get with 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');
};
  1. Set up webhook for ASSET_REPORT_READY
  2. Show progress indicator to users
  3. 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:

  1. Check error sub-code for specific reason:
    • INSUFFICIENT_FUNDS β†’ Ask user to add funds
    • PAYMENT_LIMIT_EXCEEDED β†’ Reduce amount or split payment
    • SCA_REQUIRED β†’ Re-authenticate via Link
    • PAYMENT_REJECTED β†’ Contact bank or try different account
  2. Implement payment status polling:
const payment = await plaidClient.paymentInitiationPaymentGet({
  payment_id: paymentId,
});
// Status: INITIATED, PENDING, EXECUTED, REJECTED
  1. Show user-friendly error messages
  2. 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 basePath URL

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_uri not 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:

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_id but 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:

  1. AWS/cloud provider outages
  2. Institution-side API changes
  3. DDoS attacks (rare)
  4. Deployment bugs
  5. Database issues
  6. 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:

  1. 0-15 min: Developers report issues on Twitter
  2. 15-30 min: Plaid acknowledges on status page ("Investigating")
  3. 30-90 min: Root cause identified ("Identified")
  4. 1-4 hours: Fix deployed ("Monitoring")
  5. 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:


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


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:


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:

  1. βœ… Check Plaid Status
  2. βœ… Test in Sandbox (isolate production issues)
  3. βœ… Verify API credentials and environment
  4. βœ… Check specific institution status
  5. βœ… 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

Monitor Your APIs

Check the real-time status of 100+ popular APIs used by developers.

View API Status β†’