Search code examples
node.jspaypalwebhooks

How to verify PayPal webhook in Node.js?


I have tried to set up verification as shown here and I keep getting the error: { error: 'invalid_token', error_description: 'Token signature verification failed' }.

Here is what my verification function looks like:

export const verifyPayPalWebhook = async (req: NextApiRequest) => {
  const url =
    "https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature";
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.PAYPAL_CLIENT_SECRET}`,
    },
    body: JSON.stringify({
      transmission_id: req.headers["paypal-transmission-id"],
      transmission_time: req.headers["paypal-transmission-time"],
      cert_url: req.headers["paypal-cert-url"],
      auth_algo: req.headers["paypal-auth-algo"],
      transmission_sig: req.headers["paypal-transmission-sig"],
      webhook_id: process.env.PAYPAL_WEBHOOK_ID,
      webhook_event: req.body,
    }),
  });

  const data = await res.json();

  if (!res.ok) {
    throw new Error(data.error_description);
  }
  return data.verification_status === "SUCCESS";
};

I have confirmed that both PAYPAL_CLIENT_SECRET and PAYPAL_WEBHOOK_ID environment variables are defined. The PAYPAL_CLIENT_SECRET is the 'secret' for my application and PAYPAL_WEBHOOK_ID is the webhook ID for the specific webhook.

I am testing my webhook endpoint by creating and capturing an order on my development server that will eventually notify the webhook endpoint.


Solution

  • To make REST API calls, you first need to use the secret and corresponding client_id to obtain a valid oauth2 token (which can be cached for 9 hours if desired)

    The easiest way to do it in node is to reuse the sample function from https://developer.paypal.com/docs/checkout/standard/integrate/

    // For a fully working example, please see:
    // https://github.com/paypal-examples/docs-examples/tree/main/standard-integration
    
    const { CLIENT_ID, APP_SECRET } = process.env;
    const baseURL = {
        sandbox: "https://api-m.sandbox.paypal.com",
        production: "https://api-m.paypal.com"
    };
    
    // generate an access token using client id and app secret
    async function generateAccessToken() {
      const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64")
      const response = await fetch(`${baseURL.sandbox}/v1/oauth2/token`, {
        method: "POST",
        body: "grant_type=client_credentials",
        headers: {
          Authorization: `Basic ${auth}`,
        },
      });
      const data = await response.json();
      return data.access_token;
    }