BlogPlaid Webhooks

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 Docs

1. Plaid Webhook Events

Plaid can send the following webhook events to your endpoint:

TRANSACTIONS.INITIAL_UPDATE
TRANSACTIONS.DEFAULT_UPDATE
TRANSACTIONS.HISTORICAL_UPDATE
AUTH.DEFAULT_UPDATE
IDENTITY.DEFAULT_UPDATE
ITEM.ERROR
ITEM.PENDING_EXPIRATION

2. Set Up a Test Endpoint with HookRay

Follow these steps to start receiving Plaid webhooks for testing:

  1. Go to HookRay and click "Start Testing — Free" to get your unique webhook URL.
  2. Copy the URL (e.g., https://h.hookray.com/abc123).
  3. In your Plaid dashboard, navigate to the webhook settings and paste the HookRay URL as your endpoint.
  4. Select the events you want to receive (see list above).
  5. 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:

payload.json
{
  "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

Signing details
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}
Watch out: Plaid uses public-key crypto (JWT/ES256), not HMAC. There's no shared secret — you fetch a per-message public key from Plaid using the JWT's kid. Cache the JWK by kid (it rotates rarely) to avoid an API roundtrip per webhook. Always check both the JWT signature AND the request_body_sha256 claim — the JWT alone doesn't prove the body wasn't tampered with.

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 — Free

Free 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)