Providers / April 25, 2026

How to receive Plaid webhooks reliably (transactions, items, errors)

Plaid webhook flow with JWT signature verification and key rotation

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

  • TRANSACTIONSINITIAL_UPDATE, HISTORICAL_UPDATE, DEFAULT_UPDATE, TRANSACTIONS_REMOVED, SYNC_UPDATES_AVAILABLE
  • ITEMERROR, LOGIN_REPAIRED, NEW_ACCOUNTS_AVAILABLE, PENDING_EXPIRATION, USER_PERMISSION_REVOKED, WEBHOOK_UPDATE_ACKNOWLEDGED
  • AUTH, IDENTITY, ASSETS, HOLDINGS, INVESTMENTS_TRANSACTIONS
  • PAYMENT_INITIATION for payments
  • TRANSFER for 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:

  1. Read the Plaid-Verification header — it's a JWT.
  2. Decode the JWT header (without verifying yet) to get the key ID (kid).
  3. Fetch the public key for that kid from Plaid's API: POST /webhook_verification_key/get with { "key_id": "<kid>" }.
  4. Verify the JWT signature using the public key (ES256).
  5. Verify the JWT's request_body_sha256 claim matches the actual SHA-256 of the request body (this is what binds the JWT to the body).
  6. 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 TRANSACTIONS you can use /transactions/sync to 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/sync when events are missed
  • Routing by webhook_typeTRANSACTIONS to one handler, ITEM/ERROR to 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:

  1. Create a webhook in Hooksbase with your application URL as the destination
  2. Add your verification forwarder URL to your Plaid item webhook configuration
  3. 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-id header (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_type and webhook_code — send TRANSACTIONS/DEFAULT_UPDATE to your sync agent, ITEM/ERROR to 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 agentTRANSACTIONS / DEFAULT_UPDATE events trigger an agent that pulls new transactions and classifies them into your custom taxonomy
  • Item health agentITEM / ERROR events trigger an agent that diagnoses the failure (login required, token expired, institution outage) and either auto-recovers or notifies the user
  • Reconciliation agentTRANSACTIONS_REMOVED events trigger an agent that updates downstream systems and flags anomalies in spend reports
  • Onboarding completion agentIDENTITY / DEFAULT_UPDATE events trigger an agent that finalizes KYC verification and unblocks the user's account
  • Payment status agentPAYMENT_INITIATION events trigger an agent that updates order status, notifies the customer, and posts to internal channels

Where to go next

Start free at app.hooksbase.com.

Related guides