While implementing PayPal webhooks in a Node.js application, I encountered an 401 Unauthorized
error with the message "Webhook signature verification failed". This issue arises when I make a POST
request to http://localhost:3001/paypal/subscriptions/webhook
using Postman
.
Background:
To simulate webhook events, I've been using Webhook.site to capture PayPal's webhook calls and extract necessary headers. These headers are then manually added to Postman requests to mimic the actual webhook call from PayPal. The headers include:
paypal-auth-algo
paypal-cert-url
paypal-transmission-id
paypal-transmission-sig
paypal-transmission-time
Despite ensuring the correctness of these headers, the verification of the webhook signature consistently fails.
paypalRouter.post("/subscriptions/webhook", async (req, res) => {
console.log("Received webhook event", req.body);
try {
console.log('Headers:', {
'paypal-auth-algo': req.headers['paypal-auth-algo'],
'paypal-cert-url': req.headers['paypal-cert-url'],
'paypal-transmission-id': req.headers['paypal-transmission-id'],
'paypal-transmission-sig': req.headers['paypal-transmission-sig'],
'paypal-transmission-time': req.headers['paypal-transmission-time'],
});
const webhookEvent = req.body;
console.log("Webhook event received:", webhookEvent.event_type);
const verification = {
auth_algo: req.headers['paypal-auth-algo'],
cert_url: req.headers['paypal-cert-url'],
transmission_id: req.headers['paypal-transmission-id'],
transmission_sig: req.headers['paypal-transmission-sig'],
transmission_time: req.headers['paypal-transmission-time'],
webhook_id: process.env.WEBHOOK_ID,
webhook_event: webhookEvent,
};
console.log('Final Verification Request Payload:', JSON.stringify(verification, null, 2));
console.log('Verification Payload:', verification);
const params = new URLSearchParams();
params.append("grant_type", "client_credentials");
const authResponse = await axios.post(
"https://api-m.sandbox.paypal.com/v1/oauth2/token",
params,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
auth: {
username: process.env.PAYPAL_CLIENT_ID,
password: process.env.PAYPAL_SECRET,
},
}
);
const accessToken = authResponse.data.access_token;
const verifyResponse = await axios.post(
"https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature",
verification,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
},
}
);
if (verifyResponse.data.verification_status === "SUCCESS") {
// Handle different event types as needed
if (webhookEvent.event_type === "BILLING.SUBSCRIPTION.ACTIVATED") {
// Extracting the custom_id and subscription ID from the webhook event
const userId = webhookEvent.resource.custom_id; // Adjusted based on actual data structure
const subscriptionId = webhookEvent.resource.id; // Adjusted based on actual data structure
console.log(`Attempting to confirm subscription for user ${userId} with subscription ID ${subscriptionId}`);
try {
// Updating the user's subscription status to 'confirmed'
const updatedUser = await User.findOneAndUpdate(
{ _id: userId, subscriptionId: subscriptionId },
{ $set: { subscriptionStatus: 'confirmed' }},
{ new: true }
);
if (updatedUser) {
console.log("Subscription confirmed for user:", userId);
} else {
console.log("No matching user document to update or subscription ID mismatch.");
}
return res.status(200).send('Subscription confirmed');
} catch (error) {
console.error("Error confirming subscription:", error);
return res.status(500).send("Error updating subscription status.");
}
}
} else {
console.log("Failed to verify webhook signature:", verifyResponse.data);
return res.status(401).send('Webhook signature verification failed');
}
}
}
});
Received webhook event: {}
Headers: {
'paypal-auth-algo': 'SHA...',
'paypal-cert-url': 'https://api.sandbox.paypal.com/v1/notifications/certs/CERT...',
'paypal-transmission-id': 'a5d...',
'paypal-transmission-sig': 'Rrwi...',
'paypal-transmission-time': '2024....'
}
Webhook event received: undefined
Final Verification Request Payload: {
"auth_algo": "SHA-....",
"cert_url": "https://api.sandbox.paypal.com/v1/notifications/certs/CERT-...",
"transmission_id": "a5d....",
"transmission_sig": "Rrw...",
"transmission_time": "2024....",
"webhook_id": "string",
"webhook_event": {}
}
Failed to verify webhook signature: { verification_status: 'FAILURE' }
Research and Attempts:
WEBHOOK_ID
as well.Questions:
Any guidance or insights would be greatly appreciated.
Finally, I solved the issue, as Paypal is very concerned about its webhooks security, I can't run my webhook locally with Postman, the solution was to download and set up ngrok and change my webhook URL to the URL ngrok generated for me and add "/paypal/subscriptions/webhook" at the end of the URL something like this:
https://ngrok-url/paypal/subscriptions/webhook
on "Sandbox Webhooks"