
Plaid fires webhooks for transactions arriving, item state changes, balance updates, identity verification results, payment events, and errors. They're how your application stays in sync with bank-side activity without polling.
Plaid's signature verification is more involved than most providers — they use JWTs with rotating ECDSA keys instead of static HMAC. This guide walks the verification flow, the common webhook shapes, and what production reliability requires.
What Plaid sends
Plaid webhooks are JSON POST requests. A TRANSACTIONS / DEFAULT_UPDATE event:
{
"webhook_type": "TRANSACTIONS",
"webhook_code": "DEFAULT_UPDATE",
"item_id": "wz666MBjYWTp2PDzzggYhM6oWWmBb",
"new_transactions": 17,
"environment": "production"
}
Headers:
Plaid-Verification: <JWT>
Content-Type: application/json
Common Plaid webhook shapes (organized by webhook_type):
TRANSACTIONS—INITIAL_UPDATE,HISTORICAL_UPDATE,DEFAULT_UPDATE,TRANSACTIONS_REMOVED,SYNC_UPDATES_AVAILABLEITEM—ERROR,LOGIN_REPAIRED,NEW_ACCOUNTS_AVAILABLE,PENDING_EXPIRATION,USER_PERMISSION_REVOKED,WEBHOOK_UPDATE_ACKNOWLEDGEDAUTH,IDENTITY,ASSETS,HOLDINGS,INVESTMENTS_TRANSACTIONSPAYMENT_INITIATIONfor paymentsTRANSFERfor ACH/bank transfers
The webhook_type and webhook_code together name the event; the rest of the payload depends on which event fired.
Plaid's signature scheme (JWT, not HMAC)
Where most providers sign with a static HMAC secret, Plaid signs with a JWT using ECDSA (ES256) and rotating keys. The verification flow:
- Read the
Plaid-Verificationheader — it's a JWT. - Decode the JWT header (without verifying yet) to get the key ID (
kid). - Fetch the public key for that
kidfrom Plaid's API:POST /webhook_verification_key/getwith{ "key_id": "<kid>" }. - Verify the JWT signature using the public key (ES256).
- Verify the JWT's
request_body_sha256claim matches the actual SHA-256 of the request body (this is what binds the JWT to the body). - Verify the JWT's
iat(issued-at) is within the last ~5 minutes (replay protection).
Plaid rotates keys, so you should cache fetched public keys and refresh them when you see a new kid.
This is significantly more code than verifying a Stripe HMAC. Plaid's official SDKs (plaid-node, plaid-python, etc.) ship a verifyWebhook helper — use it.
Plaid's retry policy
Plaid retries failed webhooks (non-2xx, or no response within ~10 seconds) with exponential backoff. The exact retry count and window vary by webhook type but are finite. Past the window, you have to query Plaid's API to detect missed events — the WEBHOOK_UPDATE_ACKNOWLEDGED mechanism helps, but it doesn't fully replace a relay.
This means:
- Acknowledge fast — return 2xx in under one second
- Be idempotent — Plaid will retry on slow responses
- Cache the verification keys — fetching them on every request is slow and adds load to your handler
- Have a recovery story — for
TRANSACTIONSyou can use/transactions/syncto backfill missed events; for other shapes you may need to query item state directly
DIY: minimal Plaid webhook handler in Node
import { PlaidApi, Configuration, PlaidEnvironments } from 'plaid'
const plaid = new PlaidApi(
new Configuration({
basePath: PlaidEnvironments[process.env.PLAID_ENV ?? 'sandbox'],
baseOptions: {
headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
'PLAID-SECRET': process.env.PLAID_SECRET,
},
},
}),
)
export async function POST(req: Request) {
const jwt = req.headers.get('plaid-verification')!
const body = await req.text()
// Plaid's official helper handles key fetch, ES256 verify, and body-hash check
const verified = await plaid.webhookVerify({ body, headers: { 'plaid-verification': jwt } })
if (!verified) return new Response('Bad signature', { status: 400 })
const event = JSON.parse(body)
// Idempotency: there's no single ID field on Plaid events; combine
// (item_id, webhook_type, webhook_code, environment, request timestamp)
// ...
queueWork(event)
return new Response('ok', { status: 200 })
}
Production needs more:
- A persistence layer for the composite idempotency key
- A queue for async work
- A cache for verification keys (fetching on every request is slow)
- A backfill path using Plaid's
/transactions/syncwhen events are missed - Routing by
webhook_type—TRANSACTIONSto one handler,ITEM/ERRORto an alerting flow - Multi-tenant support — one Plaid client per customer in some apps
Hooksbase: receive Plaid webhooks without rebuilding the rest
Plaid isn't one of Hooksbase's five pre-verified provider packs (Stripe, GitHub, Clerk, Slack, Resend), and Hooksbase does not forward Plaid's original verification header to your destination. If you need Plaid JWT verification, verify it in a small pre-ingest forwarder, then post the verified raw body to Hooksbase with the bearer ingest secret.
Setup:
- Create a webhook in Hooksbase with your application URL as the destination
- Add your verification forwarder URL to your Plaid item webhook configuration
- In the forwarder, verify the Plaid JWT, then POST the same raw body to the Hooksbase ingest URL with
Authorization: Bearer <ingest secret>
You get:
- Acknowledge quickly to Plaid after the forwarder verifies the request, removing the slow-handler-causes-retry-storm problem
- Idempotency for your destination — every dispatch includes a unique
webhook-idheader (Standard Webhooks-compatible) you can dedupe on across retries - Retries with exponential backoff to your endpoint after Hooksbase accepts the event
- Routing rules by
webhook_typeandwebhook_code— sendTRANSACTIONS/DEFAULT_UPDATEto your sync agent,ITEM/ERRORto an alerting flow - Payload transforms — extract just the fields your agent needs
- Deterministic replay — re-run a failed delivery with the same payload bytes while the payload is retained
- Delivery history and DLQ — particularly valuable for financial event flows where audit-ability matters
Common Plaid webhook use cases for AI agents
- Transaction categorization agent —
TRANSACTIONS / DEFAULT_UPDATEevents trigger an agent that pulls new transactions and classifies them into your custom taxonomy - Item health agent —
ITEM / ERRORevents trigger an agent that diagnoses the failure (login required, token expired, institution outage) and either auto-recovers or notifies the user - Reconciliation agent —
TRANSACTIONS_REMOVEDevents trigger an agent that updates downstream systems and flags anomalies in spend reports - Onboarding completion agent —
IDENTITY / DEFAULT_UPDATEevents trigger an agent that finalizes KYC verification and unblocks the user's account - Payment status agent —
PAYMENT_INITIATIONevents trigger an agent that updates order status, notifies the customer, and posts to internal channels
Where to go next
- How to receive Stripe webhooks reliably for HMAC-based verification (simpler scheme)
- How to receive Clerk webhooks reliably for the Standard Webhooks pattern
- Verify provider webhooks for the verification model
- How to build an AI agent for the full agent build path
Start free at app.hooksbase.com.