Plaid Account Verification: Instant Auth vs. Micro-Deposits
I've shipped both Plaid flows for real clients. Here's the honest breakdown of when instant auth bites you and when micro-deposits are actually worth the wait.
Plaid's marketing makes instant auth sound like a no-brainer. Connect your bank in seconds, no waiting, done. And for maybe 60–70% of your users it actually works like that. The other 30% will hit a wall, and if you didn't plan for it, your fallback experience is going to be ugly.
I've integrated Plaid's account verification flows for a handful of clients over the last few years — an e-commerce company doing ACH payouts to sellers, a healthcare billing system collecting patient payments, and most recently a real estate firm handling earnest money deposits. Each time I've had to make the same call: instant auth or same-day micro-deposits? The answer is never as obvious as Plaid's docs make it seem.
What These Two Flows Actually Do
Both flows produce the same end artifact: a verified processor_token (or access_token) you can hand off to a payment processor like Stripe, Dwolla, or your ACH originator to pull funds from a bank account. The difference is how you get there.
Instant auth uses Plaid Link to have the user log in to their bank directly — OAuth for banks that support it, or screen-scraped credentials for those that don't. When it works, you have a verified account in under a minute. No waiting period, no user coming back later.
Same-day micro-deposits skips the bank login entirely. The user types in their routing and account numbers manually. Plaid sends two small deposits (under $1) to that account, and the user confirms the amounts — usually within a few hours on business days, sometimes next day. Once confirmed, the account is verified.
The failure mode for instant auth is coverage. The failure mode for micro-deposits is dropoff.
The Coverage Problem With Instant Auth
Plaid supports something like 12,000 financial institutions. That sounds comprehensive until your user banks at a regional credit union in rural Montana, or a community bank that implemented online banking with a vendor that doesn't play well with OAuth. I've seen real users fail instant auth with institutions like Navy Federal, certain state employee credit unions, and a surprising number of smaller savings banks.
When instant auth fails — whether it's an unsupported institution, a login error, or the user just doesn't remember their online banking password — you need somewhere to send them. If you haven't built the micro-deposit fallback, they're stuck.
For the real estate client, this was a real problem. Their users are often older, not always tech-forward, and some bank at institutions that Plaid's instant auth handles poorly. I ended up building both flows from the start and letting Plaid Link surface the fallback naturally.
A Working Laravel Implementation
Here's roughly how I structure this in Laravel. I use Plaid's PHP SDK (via a Guzzle wrapper, since there's no official first-party PHP SDK — I typically use TomorrowIdeas/plaid-sdk-php).
// routes/web.php
Route::post('/plaid/create-link-token', [PlaidController::class, 'createLinkToken']);
Route::post('/plaid/exchange-token', [PlaidController::class, 'exchangeToken']);
Route::post('/plaid/verify-micro-deposits', [PlaidController::class, 'verifyMicroDeposits']);
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use TomorrowIdeas\Plaid\Plaid;
use TomorrowIdeas\Plaid\Entities\AccountFilters;
class PlaidController extends Controller
{
private Plaid $plaid;
public function __construct()
{
$this->plaid = new Plaid(
config('services.plaid.client_id'),
config('services.plaid.secret'),
config('services.plaid.env') // 'sandbox', 'development', or 'production'
);
}
public function createLinkToken(Request $request)
{
$user = $request->user();
$response = $this->plaid->tokens->create(
'My App',
'en',
['US'],
(string) $user->id,
['auth'], // products
[
'webhook' => config('services.plaid.webhook_url'),
// Enabling same-day micro-deposits as fallback
'auth' => [
'same_day_microdeposits_enabled' => true,
],
]
);
return response()->json(['link_token' => $response->link_token]);
}
public function exchangeToken(Request $request)
{
$request->validate(['public_token' => 'required|string']);
$exchange = $this->plaid->items->exchangeToken($request->public_token);
$accessToken = $exchange->access_token;
$itemId = $exchange->item_id;
// Get account info to check verification status
$auth = $this->plaid->auth->get($accessToken);
$account = collect($auth->accounts)->first();
$verificationStatus = $account->verification_status ?? null;
// Store the token and status for this user
$request->user()->bankAccount()->updateOrCreate(
['user_id' => $request->user()->id],
[
'plaid_access_token' => encrypt($accessToken),
'plaid_item_id' => $itemId,
'plaid_account_id' => $account->account_id,
'verification_status' => $verificationStatus ?? 'instantly_verified',
'verified_at' => $verificationStatus === null ? now() : null,
]
);
return response()->json([
'status' => $verificationStatus ?? 'instantly_verified',
'pending' => $verificationStatus === 'pending_manual_verification',
]);
}
public function verifyMicroDeposits(Request $request)
{
$request->validate([
'amounts' => 'required|array|size:2',
'amounts.*' => 'required|numeric|min:0.01|max:0.99',
]);
$bankAccount = $request->user()->bankAccount;
$accessToken = decrypt($bankAccount->plaid_access_token);
try {
$this->plaid->auth->verifyMicrodeposits(
$accessToken,
$bankAccount->plaid_account_id,
$request->amounts
);
$bankAccount->update([
'verification_status' => 'manually_verified',
'verified_at' => now(),
]);
return response()->json(['verified' => true]);
} catch (\Exception $e) {
// Plaid returns a specific error code for wrong amounts
// VERIFICATION_EXPIRED after too many attempts
return response()->json([
'verified' => false,
'message' => 'Those amounts did not match. Please try again.',
], 422);
}
}
}
The key thing here: when verification_status comes back as pending_manual_verification, the user went through the micro-deposit path. You cannot initiate an ACH debit against that account until they confirm the amounts and the status flips to manually_verified. If you skip that check, your payment processor will reject the pull and you'll spend an afternoon reading Dwolla error logs.
The Gotchas That Actually Bit Me
Webhook vs. polling for deposit confirmation. Plaid fires an AUTH webhook with type AUTOMATICALLY_VERIFIED or VERIFICATION_EXPIRED once deposits settle. Don't poll auth.get in a loop — it's wasteful and you'll eventually rate-limit yourself. Set up the webhook, update the status there, and make the user-facing UI reactive to your own database state.
The "instantly verified" gap in sandbox. In sandbox, every account hits instant auth without issue. You have to specifically use the test credentials that trigger the micro-deposit flow to test that path. I missed this early on for the healthcare client and didn't discover the micro-deposit edge cases until a production beta user hit them. Now I explicitly test both paths before any go-live.
Three attempts, then you're out. If a user enters the wrong micro-deposit amounts three times, the item enters VERIFICATION_EXPIRED status. There's no way to un-expire it. You have to delete the item and have the user start over with a fresh Link session. Make sure your UX communicates this clearly — "you have X attempts remaining" is not optional copy, it's damage control.
Credential-based vs. OAuth institutions. For credential-based institutions (the screen-scraped ones), Plaid stores the user's bank login credentials and uses them to periodically refresh account data. This is the thing privacy-conscious users push back on. OAuth institutions — the major banks mostly — never touch your credentials through Plaid, they redirect through the bank's own OAuth flow. Worth knowing which your user base is likely hitting, especially in healthcare where patient trust is a real concern.
Same-day is not always same-day. The product is called same-day micro-deposits, and it usually is — but only for deposits initiated before the ACH cutoff (typically 2–3 PM Eastern). Initiate at 4 PM ET on a Friday and your user is waiting until Monday. I've had clients ask why their user is still pending two days later. Now I set expectations in the UI based on the time of initiation.
When I'd Reach for Each
Reach for instant auth as primary with micro-deposit fallback when your users are likely tech-comfortable, bank at major institutions, and the conversion speed matters. E-commerce seller payouts, consumer fintech apps, anything where friction kills conversion. The fallback needs to exist — just don't lead with it.
Reach for micro-deposits as primary when your user base skews older, banks at non-mainstream institutions, or when the transaction amounts are high enough that the trust-building of "we sent you two small test deposits" actually helps. The real estate client was a case for this — someone wiring earnest money on a house is more comfortable with a process that feels deliberate.
I'd seriously reconsider Plaid entirely if: you're operating in a context where screen-scraping credentials would violate your compliance posture (some healthcare agreements are sensitive to this), or your user base is primarily international (Plaid is US/Canada focused and the coverage outside those markets is still thin). For pure manual ACH verification without the Plaid layer, Stripe's own micro-deposit flow or Dwolla's native verification are worth evaluating.
The Bottom Line
Instant auth is fast when it works and completely invisible when it doesn't — which is exactly the kind of failure mode that burns you in production. Build the micro-deposit fallback before you launch, not after your first support ticket. The two-day wait is annoying; shipping a broken verification flow is worse.
Need help shipping something like this? Get in touch.