I'm developing a feature in my Node.js application where I need to associate a user's ID, obtained from a JWT (JSON Web Token), with a custom_id
from PayPal after a successful payment. The user ID and the custom_id
serve different purposes in my system: the user ID identifies the logged-in user, while the custom_id
is generated by PayPal once a payment is successful. My objective is to store both these identifiers in my database for each transaction to link subscription payments directly to user accounts.
The process flow I envision is as follows:
custom_id
(e.g., "I-YXA1XYR62EKE") for the transaction.custom_id
along with the user's ID from the JWT and store both in my database, associating the payment with the user.Here is how I'm currently handling authentication and payment initiation. However, I'm stuck on effectively capturing and linking the custom_id
from PayPal with the user ID in my database after payment success.
middlewares/ auth.js
const jwt = require("jsonwebtoken");
const auth = async (req, res, next) => {
try {
const token = req.header("x-auth-token");
if (!token)
return res.status(401).json({ msg: "No auth token, access denied." });
const verified = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
if (!verified)
return res
.status(401)
.json({ msg: "Token verification failed, authorization denied." });
req.user = verified.id;
req.token = token;
next();
} catch (e) {
res.status(500).json({ error: e.message });
}
};
module.exports = auth;
routes/paypal.js
paypalRouter.post("/create-subscription", auth, async (req,res) => {
try {
const userId = req.user;
const subscriptionDetails = {
plan_id: process.env.PAYPAL_SANDBOX_BUSSINESS_SUBSCRIPTION_PLAN_ID,
custom_id: userId,
application_context: {
brand_name: "brand.com",
return_url: "http://localhost:3001/paypal/subscriptions/webhook",
cancel_url: "http://localhost:3001/paypal/cancel-subscription",
}
};
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 access_token = authResponse.data.access_token;
const response = await axios.post(
"https://api-m.sandbox.paypal.com/v1/billing/subscriptions",
subscriptionDetails,
{
headers: {
Authorization: `Bearer ${access_token}`,
},
}
);
console.log("subscriptionId from PAYPAL (e.g "I-YXA1XYR62EKE")" + response.data.id);
console.error();
return res.json({ subscriptionId: response.data.id, ...response.data });
} catch (error) {
console.log(error);
return res.status(500).json("Something goes wrong");
}
});
paypalRouter.post("/subscriptions/webhook", async (req, res) => {
try {
const webhookEvent = req.body;
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
};
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: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
}
);
if (verifyResponse.data.verification_status === "SUCCESS") {
if (webhookEvent.event_type === "PAYMENT.SALE.COMPLETED") {
console.log("Payment sale completed event received", webhookEvent);
const userId = webhookEvent.resource.custom_id; //
const subscriptionId = webhookEvent.resource.id; //
try {
await User.findByIdAndUpdate(userId, { subscriptionId: subscriptionId });
console.log("User subscription ID updated successfully");
} catch (error) {
console.error("Failed to update user subscription ID:", error);
return res.status(500).send("Failed to update user subscription ID.");
}
return res.status(200).send('Event processed');
}
} else {
console.log("Failed to verify webhook signature:", verifyResponse.data);
return res.status(401).send('Webhook signature verification failed');
}
} catch (error) {
console.error("Error handling webhook event:", error.response ? error.response.data : error.message);
return res.status(500).send("An error occurred while handling the webhook event.");
}
});
I'm unsure about the best practice for capturing the custom_id
after payment success and associating it with the user's ID in my database. Specifically, I'm looking for guidance on the following:
custom_id
from PayPal's webhook notification for a successful payment.custom_id
from PayPal with the user's ID extracted from the JWT.Any insights or examples of handling similar scenarios in Node.js applications would be greatly appreciated.
PayPal provides a custom_id (e.g., "I-YXA1XYR62EKE")
Such a value is usually called the subscription's id
in PayPal, not a custom_id
.
Looking at your code, it seems you're actually storing your userId value in the custom_id
field when creating the subscription.
paypalRouter.post("/create-subscription", auth, async (req,res) => {
try {
const userId = req.user;
//...
custom_id: userId,
That'll do fine.
When you receive a webhook, you're already grabbing both values perfectly fine:
if (webhookEvent.event_type === "PAYMENT.SALE.COMPLETED") {
console.log("Payment sale completed event received", webhookEvent);
const userId = webhookEvent.resource.custom_id; //
const subscriptionId = webhookEvent.resource.id; //
So, your question asks about "best practices" for what's next in relation to some JWT. Near as I can tell the answer is..whatever makes sense to do. You have all the relevant data, do with it what you will. If you're still unsure I'd like to see a more specific question about what isn't clear.
Perhaps it bears mentioning that webhook posts happen from PayPal to your server. Session and auth variables do not exist at that time, the user and their browser are not involved in any way.