Plaid logo
Docs
ALL DOCS

API

  • Overview
  • Libraries
  • API versioning
  • Postman Collection
  • Webhooks
    • Webhook verification
Payments and Funding
  • Auth
  • Balance
  • Identity
  • Signal
  • Transfer
  • Investments Move
  • Payment Initiation
  • Virtual Accounts
Financial Insights
  • Transactions
  • Investments
  • Liabilities
  • Enrich
KYC/AML and anti-fraud
  • Look up Dashboard users
  • Identity Verification
  • Monitor
  • Beacon (beta)
Instant Onboarding
  • Layer
Credit and Underwriting
  • Consumer Report (by Plaid Check)
  • Assets
  • Statements
  • Income
Fundamentals
  • Items
  • Accounts
  • Institutions
  • Sandbox
  • Link
  • Users
  • Consent
  • Network
Partnerships
  • Processor tokens
  • Processor partners
  • Reseller partners
Plaid logo
Docs
Close search modal
Ask Bill!
Ask Bill!
Hi! I'm Bill! You can ask me all about the Plaid API. Try asking questions like:
  • How do I fix an Item in ITEM_LOGIN_REQUIRED state?
  • How do I enable IDV selfie checks?
  • Which countries does Investments support?
Note: Bill isn't perfect. He's just a robot platypus that reads our docs for fun. You should treat his answers with the same healthy skepticism you might treat any other answer on the internet. This chat may be logged for quality and training purposes. Please don't send Bill any PII -- he's scared of intimacy. All chats with Bill are subject to Plaid's Privacy Policy.
Plaid.com
Log in
Get API Keys
Open nav

Verify webhooks

API reference for verifying webhooks

Plaid signs all outgoing webhooks so that you can verify the authenticity of any incoming webhooks to your application. A message signature is included in the Plaid-Verification header (Note: this is the canonical representation of the header field, but HTTP 1.x headers should be handled as case-insensitive, HTTP 2 headers are always lowercase). The verification process is optional and is not required for your application to handle Plaid webhooks.

The verification process requires understanding JSON Web Tokens (JWTs) and JSON Web Keys (JWKs). More information about these specifications can be found at jwt.io.

Libraries for interpreting and verifying JWKs and JWTs most likely exist in your preferred language. It is highly recommended that you utilize well-tested libraries rather than trying to implement these specifications from scratch.

Steps to verify webhooks

Extract the JWT header

Extract the Plaid-Verification HTTP header from any Plaid webhook (to get a webhook, see firing webhooks in Sandbox). The value of the Plaid-Verification header is a JWT, and will be referred to as "the JWT" in following steps.

Using your preferred JWT library, decode the JWT and extract the header without validating the signature. This functionality most likely exists in your preferred JWT library. An example JWT header is shown below.

1{
2 "alg": "ES256",
3 "kid": "bfbd5111-8e33-4643-8ced-b2e642a72f3c",
4 "typ": "JWT"
5}

Ensure that the value of the alg (algorithm) field in the header is "ES256". Reject the webhook if this is not the case.

Extract the value corresponding to the kid (key ID) field. This will be used to retrieve the JWK public key corresponding to the private key that was used to sign this request.

Get the verification key

Use the /webhook_verification_key/get endpoint to get the webhook verification key.

/webhook_verification_key/get

Get webhook verification key

Plaid signs all outgoing webhooks and provides JSON Web Tokens (JWTs) so that you can verify the authenticity of any incoming webhooks to your application. A message signature is included in the Plaid-Verification header.
The /webhook_verification_key/get endpoint provides a JSON Web Key (JWK) that can be used to verify a JWT.

webhook_verification_key/get

Request fields

client_id
string
Your Plaid API client_id. The client_id is required and may be provided either in the PLAID-CLIENT-ID header or as part of a request body.
secret
string
Your Plaid API secret. The secret is required and may be provided either in the PLAID-SECRET header or as part of a request body.
key_id
requiredstring
The key ID ( kid ) from the JWT header.
Select group for content switcher
Select Language
1const request: WebhookVerificationKeyGetRequest = {
2 key_id: keyID,
3};
4try {
5 const response = await plaidClient.webhookVerificationKeyGet(request);
6 const key = response.data.key;
7} catch (error) {
8 // handle error
9}
webhook_verification_key/get

Response fields and example

key
object
A JSON Web Key (JWK) that can be used in conjunction with JWT libraries to verify Plaid webhooks
alg
string
The alg member identifies the cryptographic algorithm family used with the key.
crv
string
The crv member identifies the cryptographic curve used with the key.
kid
string
The kid (Key ID) member can be used to match a specific key. This can be used, for instance, to choose among a set of keys within the JWK during key rollover.
kty
string
The kty (key type) parameter identifies the cryptographic algorithm family used with the key, such as RSA or EC.
use
string
The use (public key use) parameter identifies the intended use of the public key.
x
string
The x member contains the x coordinate for the elliptic curve point, provided as a base64url-encoded string of the coordinate's big endian representation.
y
string
The y member contains the y coordinate for the elliptic curve point, provided as a base64url-encoded string of the coordinate's big endian representation.
created_at
integer
The timestamp when the key was created, in Unix time.
expired_at
nullableinteger
The timestamp when the key expired, in Unix time.
request_id
string
A unique identifier for the request, which can be used for troubleshooting. This identifier, like all Plaid identifiers, is case sensitive.
1{
2 "key": {
3 "alg": "ES256",
4 "created_at": 1560466150,
5 "crv": "P-256",
6 "expired_at": null,
7 "kid": "bfbd5111-8e33-4643-8ced-b2e642a72f3c",
8 "kty": "EC",
9 "use": "sig",
10 "x": "hKXLGIjWvCBv-cP5euCTxl8g9GLG9zHo_3pO5NN1DwQ",
11 "y": "shhexqPB7YffGn6fR6h2UhTSuCtPmfzQJ6ENVIoO4Ys"
12 },
13 "request_id": "RZ6Omi1bzzwDaLo"
14}
Was this helpful?
Validate the webhook

Interpret the returned key as a JWK public key. Using your preferred JWT library, verify the JWT using the JWK. If the signature is not valid, reject the webhook. Otherwise, extract the payload of the JWT. It will look something like the JSON below.

1{
2 "iat": 1560211755,
3 "request_body_sha256": "bbe8e9..."
4}

Use the issued at time denoted by the iat field to verify that the webhook is not more than 5 minutes old. Rejecting outdated webhooks can help prevent replay attacks.

Extract the value of the request_body_sha256 field. This will be used to check the integrity and authenticity of the webhook body.

Compute the SHA-256 of the webhook body and ensure that it matches what is specified in the request_body_sha256 field of the validated JWT. If not, reject the webhook. It is best practice to use a constant time string/hash comparison method in your preferred language to prevent timing attacks.

Note that the request_body_sha256 sent in the JWT payload is sensitive to the whitespace in the webhook body and uses a tab-spacing of 2. If the webhook body is stored with a tab-spacing of 4, the hash will not match.

Example implementation

The following code shows one example method that can be used to verify webhooks sent by Plaid and cache public keys.

Select Language
1const compare = require('secure-compare');
2const { jwtDecode } = require('jwt-decode'); // syntax for jwtDecode 4.0 or later
3const JWT = require('jose');
4const sha256 = require('js-sha256');
5const plaid = require('plaid');
6
7// Single cached key instead of a Map
8let cachedKey = null;
9
10const plaidClient = new plaid.Client({
11 clientID: process.env.CLIENT_ID,
12 secret: process.env.SECRET,
13 env: plaid.environments.sandbox,
14});
15
16const verify = async (body, headers) => {
17 const signedJwt = JSON.parse(headers)['plaid-verification'];
18 const decodedToken = jwtDecode(signedJwt);
19 const decodedTokenHeader = jwtDecode(signedJwt, { header: true });
20 const currentKeyID = decodedTokenHeader.kid;
21
22 // Fetch key if not already cached
23 if (!cachedKey) {
24 try {
25 const response = await plaidClient.webhookVerificationKeyGet({
26 key_id: currentKeyID,
27 });
28 cachedKey = response.data.key;
29 } catch (error) {
30 return false;
31 }
32 }
33
34 // If key is still not set, verification fails
35 if (!cachedKey) {
36 return false;
37 }
38
39 // Validate the signature and iat
40 try {
41 const keyLike = await JWT.importJWK(cachedKey);
42 // This will throw an error if verification fails
43 await JWT.jwtVerify(signedJwt, keyLike, {
44 maxTokenAge: '5 min',
45 });
46 } catch (error) {
47 return false;
48 }
49
50 // Compare hashes
51 const bodyHash = sha256(body);
52 const claimedBodyHash = decodedToken.request_body_sha256;
53 return compare(bodyHash, claimedBodyHash);
54};
Was this helpful?
Developer community
GitHub
GitHub
Stack Overflow
Stack Overflow
YouTube
YouTube
Discord
Discord