Plaid logo
Docs
ALL DOCS

API

  • Overview
  • Libraries
  • API versioning
  • Postman Collection
  • Webhooks
Product API reference
  • Transactions
  • Auth
  • Balance
  • Identity
  • Assets
  • Investments
  • Liabilities
  • Payment Initiation
  • Virtual Accounts
  • Transfer (beta)
  • Income
  • Identity Verification
  • Monitor
  • Signal
  • Enrich
Other API reference
  • Item endpoints and webhooks
  • Account endpoints and schemas
  • Institution endpoints
  • Token flow and endpoints
  • Processor endpoints
  • Sandbox endpoints
  • Reseller partner endpoints
Plaid logo
Docs
Plaid.com
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.

Copy
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 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 and example

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
Copy
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.
Copy
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. 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.

Copy
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.

Caching and key rotation

It is recommended that your application caches the public key for a given key ID, but for no more than 24 hours. This reduces the likelihood of using an expired key to validate incoming webhooks.

It may be possible that a key rotation will take place. If your application encounters a webhook with a key ID that it does not have in its cache, it should make an API request to fetch the associated public key. In addition, your application must refetch any keys from the API that are currently in the cache but not yet expired. This helps to ensure that expired keys are not being used for live validation of webhooks by refreshing the expired_at field.

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
Copy
1const compare = require('secure-compare');
2const jwt_decode = require('jwt-decode');
3const JWT = require('jose');
4const sha256 = require('js-sha256');
5const plaid = require('plaid');
6
7// Cache for webhook validation keys.
8const KEY_CACHE = new Map();
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 = jwt_decode(signedJwt);
19 // Extract the JWT header
20 const decodedTokenHeader = jwt_decode(signedJwt, { header: true });
21 // Extract the kid value from the header
22 const currentKeyID = decodedTokenHeader.kid;
23
24 //If key not in cache, update the key cache
25 if (!KEY_CACHE.has(currentKeyID)) {
26 const keyIDsToUpdate = [];
27 KEY_CACHE.forEach((keyID, key) => {
28 // We will also want to refresh any not-yet-expired keys
29 if (key.expired_at == null) {
30 keyIDsToUpdate.push(keyID);
31 }
32 });
33
34 keyIDsToUpdate.push(currentKeyID);
35
36 for (const keyID of keyIDsToUpdate) {
37 const response = await plaidClient
38 .webhookVerificationKeyGet({
39 key_id: keyID,
40 })
41 .catch((err) => {
42 // decide how you want to handle unexpected API errors,
43 // e.g. retry later
44 return false;
45 });
46 const key = response.data.key;
47 KEY_CACHE.set(keyID, key);
48 }
49 }
50
51 // If the key ID is not in the cache, the key ID may be invalid.
52 if (!KEY_CACHE.has(currentKeyID)) {
53 return false;
54 }
55
56 // Fetch the current key from the cache.
57 const key = KEY_CACHE.get(currentKeyID);
58
59 // Reject expired keys.
60 if (key.expired_at != null) {
61 return false;
62 }
63
64 // Validate the signature and iat
65 try {
66 const keyLike = await JWT.importJWK(key);
67 // This will throw an error if verification fails
68 const { payload } = await JWT.jwtVerify(signedJwt, keyLike, {
69 maxTokenAge: '5 min',
70 });
71 } catch (error) {
72 return false;
73 }
74
75 // Compare hashes.
76 const bodyHash = sha256(body);
77 const claimedBodyHash = decodedToken.request_body_sha256;
78 return compare(bodyHash, claimedBodyHash);
79};
Was this helpful?
Developer community
GitHub
GitHub
Stack Overflow
Stack Overflow
YouTube
YouTube
Twitter
Twitter
Discord
Discord