Plaid · event: TRANSACTIONS / SYNC_UPDATES_AVAILABLE ·
content type: application/json
This is a representative sample — fake IDs, real structure. To see a payload with your data, point the provider at a capture URL (button below).
One click creates a free capture bin with this exact payload already in it — pretty-printed, headers inspectable, replayable to your own endpoint:
Then paste the bin's URL into Plaid's webhook settings to capture a real delivery. No signup.
content-type: application/json plaid-verification: eyJhbGciOiJFUzI1NiIsImtpZCI6IjZlNWY3ZDhjLTJhOTEtNGMxMS05ZjNlLTU4YTFiMmM0ZDdlMCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3ODgwODI4ODcsInJlcXVlc3RfYm9keV9zaGEyNTYiOiJlMjNiMzQ3ZjUzOGJhODQyM2UxYTdlZGEyMzY1NGU3YWIyZGJlMmRkYTZkMDI0NjQyYWVmODk4ZWNiZGQ4NTRjIn0.8lyLgNk7YgW3L5C4F-ADEa6zwTiE26ubTN6lJYqR6Bqkgb6idC-ZZpICs_BbmsRwCLwSVBuVhC9n53HV_esqkg
{
"webhook_type": "TRANSACTIONS",
"webhook_code": "SYNC_UPDATES_AVAILABLE",
"item_id": "kg7pQrPzKvhqLbGXe4wMSNn3o5eWzZC7DKM4z",
"user_id": "usr_4vQx2KuZ2x8JDq",
"initial_update_complete": true,
"historical_update_complete": false,
"environment": "production"
}
Plaid-Verification header is an ES256 (ECDSA P-256) JSON
Web Token. Plaid's documented steps: decode the JWT header without validating;
reject if alg ≠ ES256 (never let the token
pick its own algorithm); take the kid and fetch the matching public JWK
from /webhook_verification_key/get (an authenticated API call — so only
you, holding your Plaid secret, can even fetch the key); then verify the
signature. Generic HMAC testers — and a capture bin — structurally can't verify this
scheme, because verification requires that authenticated key retrieval./webhook_verification_key/get
response):
{ "alg": "ES256", "crv": "P-256",
"kid": "6e5f7d8c-2a91-4c11-9f3e-58a1b2c4d7e0",
"kty": "EC", "use": "sig",
"x": "XHrNHozTSQZRVO8EUGKSGD04qg_brIzS23i8ZUZNLzM",
"y": "uOrZVjjDtlTBhwb_hoYM5RRgdtKonzaVFUcebRmlWzM" }
The JWT payload is just {"iat":1788082887,"request_body_sha256":"e23b347f…"}
— SHA-256 the raw body bytes exactly as shown above and compare. Re-serialized
JSON changes the hash: capture the raw bytes first.iat is more than 5 minutes old (replay protection — note
this frozen example is, by definition, always "too old"), and reject if the computed
body SHA-256 doesn't equal request_body_sha256. Both checks are on you;
the JWT library only does the signature.429
with Retry-After (seconds, HTTP date, or ISO 8601) is honored up to a
4-hour wait.webhook_type + webhook_code, then
pull. SYNC_UPDATES_AVAILABLE fires after the initial 30-day
fetch, after the historical backfill (watch the two *_complete booleans),
and on scheduled updates — the actual transactions come from calling
/transactions/sync, not from the webhook. Deliveries originate from four
published static IPs (52.21.26.131, 52.21.47.157, 52.41.247.19, 52.88.82.239 — subject
to change) and your HTTPS endpoint needs a valid certificate.Reproduce this delivery against any endpoint (your handler, a bin, staging):
$ curl -X POST https://your-endpoint.example/hook \
-H 'content-type: application/json' \
-H 'plaid-verification: eyJhbGciOiJFUzI1NiIsImtpZCI6IjZlNWY3ZDhjLTJhOTEtNGMxMS05ZjNlLTU4YTFiMmM0ZDdlMCIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3ODgwODI4ODcsInJlcXVlc3RfYm9keV9zaGEyNTYiOiJlMjNiMzQ3ZjUzOGJhODQyM2UxYTdlZGEyMzY1NGU3YWIyZGJlMmRkYTZkMDI0NjQyYWVmODk4ZWNiZGQ4NTRjIn0.8lyLgNk7YgW3L5C4F-ADEa6zwTiE26ubTN6lJYqR6Bqkgb6idC-ZZpICs_BbmsRwCLwSVBuVhC9n53HV_esqkg' \
-d '{
"webhook_type": "TRANSACTIONS",
"webhook_code": "SYNC_UPDATES_AVAILABLE",
"item_id": "kg7pQrPzKvhqLbGXe4wMSNn3o5eWzZC7DKM4z",
"user_id": "usr_4vQx2KuZ2x8JDq",
"initial_update_complete": true,
"historical_update_complete": false,
"environment": "production"
}'