How to Test Plaid Webhooks
Plaid webhooks notify you when transactions refresh, items need attention, or identity and auth data changes on linked accounts.
Looking for the broader picture? See the 7 best webhook testing tools (2026), or if you're already on Webhook.site, the 60-second migration to HookRay.
Plaid Official Webhook Docs1. Plaid Webhook Events
Plaid can send the following webhook events to your endpoint:
TRANSACTIONS.INITIAL_UPDATETRANSACTIONS.DEFAULT_UPDATETRANSACTIONS.HISTORICAL_UPDATEAUTH.DEFAULT_UPDATEIDENTITY.DEFAULT_UPDATEITEM.ERRORITEM.PENDING_EXPIRATION2. Set Up a Test Endpoint with HookRay
Follow these steps to start receiving Plaid webhooks for testing:
- Go to HookRay and click "Start Testing — Free" to get your unique webhook URL.
- Copy the URL (e.g.,
https://h.hookray.com/abc123). - In your Plaid dashboard, navigate to the webhook settings and paste the HookRay URL as your endpoint.
- Select the events you want to receive (see list above).
- Trigger a test event — HookRay will show the incoming webhook in real-time.
3. Sample Plaid Webhook Payload
Here's an example of what a Plaid webhook payload looks like:
{
"webhook_type": "TRANSACTIONS",
"webhook_code": "DEFAULT_UPDATE",
"item_id": "item_4NW9bQK2m9Tr8A",
"new_transactions": 3,
"removed_transactions": [],
"environment": "sandbox"
}4. How to Verify Plaid Webhook Signatures
- Algorithm
- ES256 (JWT, ECDSA P-256 + SHA-256)
- Header
Plaid-Verification- Encoding
- base64
Plaid is unique — the header value is a full JWT, not an HMAC digest. The JWT header has `alg=ES256` and a `kid`. Fetch the public key with `POST /webhook_verification_key/get` (sending the kid), verify the JWT, then check `request_body_sha256` matches SHA-256 of the body.
Node.js (Express)
// npm i jsonwebtoken jwk-to-pem
import jwt from 'jsonwebtoken';
import jwkToPem from 'jwk-to-pem';
import crypto from 'node:crypto';
import express from 'express';
import { PlaidApi } from 'plaid';
const plaidClient = new PlaidApi(/* ... */);
const app = express();
app.post(
'/webhooks/plaid',
express.raw({ type: 'application/json' }),
async (req, res) => {
const token = req.headers['plaid-verification'] as string | undefined;
if (!token) return res.status(401).send('missing token');
// Inspect JWT header to get kid.
const [headerB64] = token.split('.');
const header = JSON.parse(Buffer.from(headerB64, 'base64').toString());
if (header.alg !== 'ES256') return res.status(401).send('wrong alg');
// Fetch JWK from Plaid using the kid.
const { data } = await plaidClient.webhookVerificationKeyGet({ key_id: header.kid });
const pem = jwkToPem(data.key as jwkToPem.JWK);
// Verify the JWT signature + iat freshness (5 min tolerance).
const decoded = jwt.verify(token, pem, {
algorithms: ['ES256'],
maxAge: '5m',
}) as { request_body_sha256: string };
// Check the body hash matches what was signed.
const bodyHash = crypto.createHash('sha256').update(req.body).digest('hex');
if (
bodyHash.length !== decoded.request_body_sha256.length ||
!crypto.timingSafeEqual(
Buffer.from(bodyHash),
Buffer.from(decoded.request_body_sha256),
)
) {
return res.status(403).send('body hash mismatch');
}
res.json({ ok: true });
},
);Python (FastAPI)
# pip install python-jose plaid-python
import hashlib, os, time
from jose import jwt as jose_jwt
from plaid.api import plaid_api
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
plaid_client = plaid_api.PlaidApi(...) # configured per Plaid SDK docs
@app.post("/webhooks/plaid")
async def plaid_webhook(request: Request):
body = await request.body()
token = request.headers.get('plaid-verification')
if not token:
raise HTTPException(status_code=401, detail='missing token')
# 1. Inspect header to get kid + confirm ES256.
header = jose_jwt.get_unverified_header(token)
if header.get('alg') != 'ES256':
raise HTTPException(status_code=401, detail='wrong alg')
# 2. Fetch JWK from Plaid.
resp = plaid_client.webhook_verification_key_get({'key_id': header['kid']})
jwk = resp['key']
# 3. Verify JWT (ES256) — python-jose accepts JWK dicts directly.
claims = jose_jwt.decode(token, jwk, algorithms=['ES256'])
# 4. Replay window + body hash check.
if abs(int(time.time()) - claims.get('iat', 0)) > 60 * 5:
raise HTTPException(status_code=401, detail='stale request')
body_hash = hashlib.sha256(body).hexdigest()
if body_hash != claims.get('request_body_sha256'):
raise HTTPException(status_code=403, detail='body hash mismatch')
return {'ok': True}Capture a real Plaid webhook with HookRay first, then replay the captured request against your verifier locally — that way you can iterate on the verification code without re-triggering events in Plaid. Read Plaid's official signing docs for the canonical reference, or see the cross-service signature verification guide for Ruby and timing-safe comparison patterns.
5. Frequently Asked Questions
How do I test Plaid webhooks without deploying?
Use HookRay to get an instant public webhook URL. Paste it into your Plaid dashboard's webhook configuration, trigger an event, and watch the payload arrive in real time. No code, no ngrok, no deployment required. The free tier captures 100 requests per month and works on all Plaid event types.
Why aren't my Plaid webhooks arriving?
The four most common causes: (1) the endpoint URL isn't publicly accessible — Plaid can't reach localhost; (2) the wrong events are subscribed in your Plaid dashboard; (3) signature verification is rejecting the request before your handler runs; (4) Plaid can't reach your server because of a firewall, expired SSL certificate, or wrong DNS. Use HookRay's URL to isolate which of these four is failing — if HookRay receives the webhook, the problem is in your handler. If HookRay doesn't, the problem is in Plaid configuration.
Why am I getting 400 or 500 errors from my Plaid webhook?
Plaid reports the response status your endpoint returned. HookRay accepts any payload and returns 200 OK by default, so if you see 400/500 in your Plaid dashboard while pointing at HookRay, the issue is in Plaid's configuration (wrong event, malformed signing secret, etc.). If you point at your own endpoint and get 400/500, the issue is in your handler — capture the request with HookRay, replay it locally, and debug from the captured payload.
How do I verify Plaid webhook signatures?
Plaid signs each webhook request with a shared secret. Capture the raw headers and body using HookRay, then verify the signature in your application using Plaid's SDK or a standard HMAC library. Once verification works against HookRay-captured data, you can safely deploy. Plaid's docs (linked above) cover the exact signing algorithm.
Can I replay a captured Plaid webhook?
Yes — HookRay's replay feature re-sends any captured webhook to a different endpoint with one click. This is the fastest way to fix a buggy handler: capture the payload once, fix your code, and replay until it works. No need to re-trigger the event in Plaid.
6. Next Steps
- Use HookRay's webhook replay feature to re-send captured webhooks while building your handler
- Enable smart parsing (Pro plan) to see Plaid-specific fields highlighted automatically
- Check the Plaid webhook documentation for the complete event reference
Ready to test Plaid webhooks?
Get a free webhook URL in 5 seconds. No signup required.
Start Testing Plaid Webhooks — FreeFree PDF: Webhook Testing Cheat Sheet 2026
One-page reference for 50+ APIs — canonical events, signing methods, sample payloads. Print it, pin it, share it.
📄 Download the cheat sheet (PDF, 180KB)