javascriptnode.jsmongodbaxiospaypal-rest-sdk

How to share `response` between routes in Express for PayPal subscription?


I'm working on integrating PayPal subscriptions into my Node.js application using Express and encountered a problem sharing the subscription ID obtained from PayPal's /create-subscription route with the /execute-subscription route.

The goal is to capture the subscriptionId from the response of the /create-subscription route:

const response = await axios.post("https://api-m.sandbox.paypal.com/v1/billing/subscriptions", subscriptionDetails, {
  headers: {
    Authorization: `Bearer ${access_token}`,
  },
});

And use it in the /execute-subscription route to activate the subscription:

const subscriptionId = response.data.id; 

However, I'm not sure how to properly pass subscriptionId between these two routes. Directly accessing response.data.id in the /execute-subscription route results in an undefined error, likely because response is not in scope.

Code Snippet

paypalRouter.post("/create-subscription", async (req,res) => {
  try {
    const subscriptionDetails = {
      plan_id: process.env.PAYPAL_SANDBOX_BUSSINESS_SUBSCRIPTION_PLAN_ID,
     
      application_context: {
        brand_name: "brand",
        return_url: "http://localhost:3001/paypal/execute-subscription",
        cancel_url: "http://localhost:3001/paypal/cancel-subscription",
      }
    };

    // Generar un token de acceso
    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(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.get("/execute-subscription", async (req, res) => {
  const { token } = req.query; // El token de la suscripción que PayPal envía de vuelta

  try {
    // Paso 1: Obtener el Token de Acceso
    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 subscriptionId = response.data.id; 
    const executeResponse = 
    await axios.post(`https://api-m.sandbox.paypal.com/v1/billing/subscriptions/${subscriptionId}/activate`, {}, {
      headers: {
        Authorization: `Bearer ${access_token}`,
        'Content-Type': 'application/json'
      },
    });

    console.log("Subscription confirmed:", executeResponse.data);
    console.error();
        res.send("Subscription successful!");
  } catch (error) {
    console.error("Error executing subscription:", error.response ? error.response.data : error.message);
    res.status(500).send("An error occurred while executing the subscription.");
  }
});



Solution

  • Subscriptions do not require activation unless you specify application_context.user_action = 'CONTINUE'. Doing so also requires you to show an order review page before activation, otherwise it is misleading to the payer.

    The default behavior of SUBSCRIBE_NOW avoids needing a review page before activation, and so is generally preferable. However, with that default behavior the subscription will activate at PayPal and any return to your site after activation may or may not occur. The solution is to create a webhook URL listener for the event PAYMENT.SALE.COMPLETED. All subscription logic can be built off just that event to refresh the subscription's good-through date (however you track it), no other event names are necessary nor important, though you can subscribe and log others and decide if you ever want to do something with them later (such as react to refunds/reversals in some automated way, not important at this stage)

    Additionally, the behavior of redirecting away from your website and back to a return_url is an old integration pattern, for old websites. Instead, use the JS SDK for the subscription's approval. The subscription can be created from the JS using just the plan_id , or alternatively the createSubscription function can fetch the ID of a created subscription from your backend, calling a route that implements the logic in your question. Such fetching of an API call result (instead of creating with JS) is generally unnecessary but you can do it if you want.

    An easy way to get a working JS subscribe button is via http://www.sandbox.paypal.com/billing/plans , which can then be adapted to your needs. Make sure the plan_id you end up using was created with the client_id the JS SDK is being loaded with.