Back

Using Webhooks in Next.js

Using Webhooks in Next.js

Webhooks play a crucial role in various aspects of web development. You may wonder why they are vital and how to use them. This article will explain webhooks and how to implement them in Next.js. You can integrate webhooks with Node.js and other backend frameworks or services collaborating with the front end, and this article will show you how.

A webhook is a method for web applications to communicate with each other in real time. You can have multiple web applications where one can send HTTP requests to another when a specific event is triggered.

For example, you integrate a payment system into your web application that handles payments, refunds, and cancellations. In this type of integration, you have a payment system or gateway and your web application. When a user completes a payment transaction through the payment gateway, an event occurs, and the payment gateway sends an HTTP request to your application. However, to use that payment request, your web application needs an endpoint that can receive and process the request or send it as a notification to the user.

This endpoint acts as a webhook between the payment gateway and your web application, communicating almost in real time and notifying the user of successful payments, refunds, or unsuccessful payments.

Benefits of webhooks

A web application or system which has a webhook or uses a webhook has several benefits. For instance:

  • Real-Time Updates: Webhooks give real-time notifications for every event.
  • Security: Most times, before webhook notifications are received, they are verified to confirm the authenticity of incoming requests such as signatures or tokens.
  • Automated Workflows: Webhooks help handle automated workflow based on business logic or events that occur or are triggered.
  • Improved User Experience: Webhooks provides users with up-to-date information on events such as payments, collaborative features, and messaging notifications.

Project Overview

To show what a webhook is, we will build a payment application using the following:

  • Next.js
  • Braintree payment gateway
  • Ngrok

I will explain in detail how you can make payments, transfers, and refunds. Additionally, I will guide you through setting up the Braintree webhook.

Note: We will be using the Braintree sandbox so that you can simulate the payments, and not all features may be available.

Braintree sandbox is a test environment for development purposes, not production. You can carry out tests in it. It gives you a private key, merchant ID, and public key to use their sandbox API.

Setting up Braintree Sandbox

To set up Braintree Sandbox, head to their website and click the signup button. All you need is:

  • Your first and last name.
  • Your business name.
  • Your Business location.
  • And your email.

After signing up, the website will direct you to your control panel. You should also be able to see your merchant ID, private key, and public key. Save them somewhere secure on your PC.

Before starting the project, you must set up Ngrok. If you have it on your PC, you can skip this. If you don’t have Ngrok installed, head to their site and click sign up for free. Then, download and install the version for your PC.

Setting up the project

This project is going to be built using Next.js. Most people may wonder why someone would make a payment system on Next.js. Next.js doesn’t just render pages on the server side but also has API routes that operate similarly to Node.js for the backend, abstracting the API calls from the browser or the client.

Head to my GitHub repo to clone the project and follow along. The instructions for setting up the project on your PC are written there.

Making payments

We must create the payment page first to make payments using the Braintree sandbox. This is done inside the app directory in the root folder. Inside the app directory, create a folder called payment page (you can name it what you want); inside the folder, create a file called page.js. In the updated Nextjs version, this is how pages are created.

After that, write the following code:

"use client";
// pages/payment.js
import { useState, useEffect } from "react";
import styles from "../style/pages.module.css";
import DropIn from "braintree-web-drop-in";
import "../style/dropin.css";
import accounting from "accounting";

const PaymentPage = () => {
  const [paymentResult, setPaymentResult] = useState("");
  const [clientToken, setClientToken] = useState("");
  const [dropInInstance, setDropInInstance] = useState(null);
  const [amount, setAmount] = useState("");

  useEffect(() => {
    const fetchClientToken = async () => {
      try {
        const response = await fetch("/api/braintreeConfig/braintreeConn"); // Adjust the path based on your API route
        const data = await response.json();
        setClientToken(data.clientToken);
      } catch (error) {
        console.error("Error fetching Braintree client token:", error);
      }
    };

    fetchClientToken();
  }, []);

  console.log(clientToken);

  useEffect(() => {
    if (clientToken) {
      // Initialize DropIn instance when clientToken is available
      DropIn.create(
        {
          authorization: clientToken,
          container: "#dropin-container", // Specify the container element
        },
        (error, instance) => {
          if (error) {
            console.error("Error creating DropIn instance:", error);
            return;
          }
          setDropInInstance(instance);
        },
      );
    }
  }, [clientToken]);

  const handlePayment = async () => {
    try {
      if (!dropInInstance) {
        console.error("DropIn instance not available");
        return;
      }

      const { nonce } = await dropInInstance.requestPaymentMethod();
      // Send the nonce to your server for payment processing
      await sendPaymentToServer(nonce);
      setPaymentResult("Payment successful!");
    } catch (error) {
      console.error("Error processing payment:", error);
      setPaymentResult(
        "Payment failed. Please check your information and try again.",
      );
    }
  };

  const sendPaymentToServer = async (nonce) => {
    try {
      const response = await fetch("/api/braintreeConfig/paymentProcess", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          nonce,
          amount,
        }),
      });

      if (response.ok) {
        // Payment successful, handle accordingly
        setPaymentResult("Payment successful!");
      } else {
        // Payment failed, handle accordingly
        setPaymentResult(
          "Payment failed. Please check your information and try again.",
        );
      }
    } catch (error) {
      console.error("Error sending payment to server:", error);
    }
  };

  const handleInputChange = (e) => {
    const value = e.target.value;
    const numericValue = value.replace(/[^0-9]/g, "");
    const formattedValue = accounting.formatMoney(numericValue / 100, "$", 2);

    setAmount(formattedValue);
  };
  console.log(amount);
  return (
    <div>
      {clientToken ? <div id="dropin-container"></div> : <div>Loading...</div>}
      {/* Additional form fields */}
      <div className={styles.main}>
        <input
          type="text"
          value={amount}
          placeholder="$0.00"
          onChange={handleInputChange}
          required
        />
        <button
          className={styles.button}
          onClick={handlePayment}
          disabled={!clientToken}
        >
          Submit Payment
        </button>
      </div>
      <div>{paymentResult}</div>
    </div>
  );
};

export default PaymentPage;

Let’s look through the code. The part in the return function contains the code to be rendered on the screen. It includes a condition that runs if the clientToken is true and holds a value; otherwise, it shows loading on the screen. The part not in a conditional block is an input form that will take in the amount value to be paid and the button to send the payment nonce and amount to the server.

The last part will display whether the payment was successful or not. Here is the code part:

return (
  <div>
    {clientToken ? <div id="dropin-container"></div> : <div>Loading...</div>}
    {/* Additional form fields */}
    <div className={styles.main}>
      <input
        type="text"
        value={amount}
        placeholder="$0.00"
        onChange={handleInputChange}
        required
      />
      <button
        className={styles.button}
        onClick={handlePayment}
        disabled={!clientToken}
      >
        Submit Payment
      </button>
    </div>
    <div>{paymentResult}</div>
  </div>
);

The above code is just the part rendered on the screen. The primary function is done by getting the clientToken from the server.

useEffect(() => {
  const fetchClientToken = async () => {
    try {
      const response = await fetch("/api/braintreeConfig/braintreeConn"); // Adjust the path based on your API route
      const data = await response.json();
      setClientToken(data.clientToken);
    } catch (error) {
      console.error("Error fetching Braintree client token:", error);
    }
  };

  fetchClientToken();
}, []);

I put it in a useEffect hook because the clientToken state is used for conditional rendering and forms a part of the application’s life cycle. That is, something is to be rendered when it is true or false. In the code above, the clientToken is gotten from the braintreeConn API route in Nextjs and stored in a state.

The next part of the code creates a drop-in UI for inputting users’ card details. The drop-in UI is just a form for inputting card values such as card number, expiring date, etc. It is initialized in a useEffect hook and created using the DropIn.create function. It is obtained from the Braintree web drop-in package.

import DropIn from 'braintree-web-drop-in';
useEffect(() => {
  if (clientToken) {
    // Initialize DropIn instance when clientToken is available
    DropIn.create(
      {
        authorization: clientToken,
        container: "#dropin-container", // Specify the container element
      },
      (error, instance) => {
        if (error) {
          console.error("Error creating DropIn instance:", error);
          return;
        }
        setDropInInstance(instance);
      },
    );
  }
}, [clientToken]);

For the drop-in UI to be successfully created, you need to add the clientToken for authorization and the name of the UI container, which will be used as the ID for the form to be rendered on the screen. The image below shows what the drop-in UI looks like.

BT_sandbox10

If it returns an error, it logs or sets the instance using the setDropInInstance function. Notice the useEffect hook is run once only when the clientToken is gotten.

After that part, the next part of the code is the part to handle payment:

const handlePayment = async () => {
  try {
    if (!dropInInstance) {
      console.error("DropIn instance not available");
      return;
    }

    const { nonce } = await dropInInstance.requestPaymentMethod();
    // Send the nonce to your server for payment processing
    await sendPaymentToServer(nonce);
    setPaymentResult("Payment successful!");
  } catch (error) {
    console.error("Error processing payment:", error);
    setPaymentResult(
      "Payment failed. Please check your information and try again.",
    );
  }
};

const sendPaymentToServer = async (nonce) => {
  try {
    const response = await fetch("/api/braintreeConfig/paymentProcess", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        nonce,
        amount,
      }),
    });

    if (response.ok) {
      // Payment successful, handle accordingly
      setPaymentResult("Payment successful!");
    } else {
      // Payment failed, handle accordingly
      setPaymentResult(
        "Payment failed. Please check your information and try again.",
      );
    }
  } catch (error) {
    console.error("Error sending payment to server:", error);
  }
};

This is another central part of the code that sends the payment nonce and amount to the payment API route when the payment button is clicked.

It has two functions. The first is handlePayment, which gets the payment nonce from the dropInInstance using the requestPaymentMethod and calls the sendPaymentToServer. The second sendPaymentToServer function sends data to the payment route, and the data is the nonce and the amount to be paid.

The nonce is a secure token used only once. It is generated by Braintree on the client side and used to make secure payments. It is sent to the server and used to process payments. Let’s look at how to make the Nextjs API route interact with Braintree, send information to the Braintree sandbox, and process payments.

To do so, you have to install and import Braintree, but since you are using the cloning my GitHub repository, you did;

npm install

But just in case you want to know how it was installed, in your terminal inside your project root directory type;

npm install braintree

This installs the Braintree package so that you can use the Braintree SDK for the sandbox or production.

After that, create a pages folder in the root directory. Then, create an API folder, inside create a paymentProcess.js file, and write the following code;

import braintree from "braintree";

const gateway = new braintree.BraintreeGateway({
  environment: braintree.Environment.Sandbox,
  merchantId: process.env.MerchantId,
  publicKey: process.env.PublicKey,
  privateKey: process.env.PrivateKey,
});

export default async function handler(req, res) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method Not Allowed" });
  }

  const { nonce, amount: amountValue } = req.body;
  console.log(nonce, amountValue);
  const extractAmount = amountValue.match(/\d+/);
  const amount = extractAmount ? parseInt(extractAmount[0], 10) : null;
  console.log(amount);

  try {
    const result = await gateway.transaction.sale({
      amount,
      paymentMethodNonce: nonce,
      options: {
        submitForSettlement: true,
      },
    });

    if (result.success) {
      res.status(200).json({ success: true });
    } else {
      console.error("Transaction failed:", result.message);
      res.status(500).json({ error: "Transaction failed" });
    }
  } catch (error) {
    console.error("Error processing payment:", error);
    res.status(500).json({ error: "Internal Server Error" });
  }
}

The gateway variable is initialized by connecting to the Braintree sandbox environment and authenticated using your merchantId, privateKey, and publicKey. You have to create a .env.local file that contains these keys.

The API handler function receives a POST request, which contains the body with the nonce and amount of data. The amount of data is formatted to extract only the numbers and not the dollar sign.

Then, inside a try-catch block, the payment transaction is created using the gateway.transaction.sale function.

Note: when passing the parameters inside the function, use the parameters amount, paymentMethodNonce, and Options; otherwise, you may receive an error.

Lastly, let’s write the code to get the clientToken. Remember, our client-side drop-in UI cannot be initialized or created without the clientToken. To do so, in the API folder created earlier, create a braintreeConn.js file and write the following code:

// pages/api/braintree.js
import braintree from "braintree";

export default async function handler(req, res) {
  const gateway = new braintree.BraintreeGateway({
    environment: braintree.Environment.Sandbox, // Change to Production for live environment
    merchantId: process.env.MerchantId,
    publicKey: process.env.PublicKey,
    privateKey: process.env.PrivateKey,
  });

  const clientToken = await gateway.clientToken.generate();

  console.log(clientToken);

  res.status(200).json({ clientToken: clientToken.clientToken });
}

If you see a getSession or session in the GitHub code, please ignore it. It’s not needed.

The image below shows the clientToken, a bunch of strings.

my clienttoken two

Like the previous code, you have to initialize the gateway with your API keys and ID, then get the clientToken using the function gateway.clientToken.generate() and send it as a response to the client side.

After that, you should be able to make a successful payment using Braintree. I’ll show some screenshots of mine. The image below shows the drop-in UI form with the card details inputted into it.

fill form braintree

When the payment button is clicked and the payment is successful, you get a screen like the image below.

BT_sandbox11

Inside the Braintree sandbox, if you click on transactions and scroll down to search, you will get to a page similar to the image below.

BT_sandbox18

The payment has been successfully disbursed to the payee’s account if the transaction is settled. While submitted for settlement means Braintree is processing it before it is successfully paid. The GIF below shows how the payment works. c6c3f33b-a6ef-4fb9-bcd6-0c8aed32e244-ezgif.com-video-to-gif-converter

Handling Transfer and Refund Payments

To handle transfers, we’ll create a transfer folder:

  • Inside the app folder, create a transferForm folder.
  • Inside the folder, create a file called page.js. After making the necessary folders, write the following codes in them:
// State variables for handling user input and response data
const [amount, setAmount] = useState('');
const [clientToken, setClientToken] = useState('');
const [recipientEmail, setRecipientEmail] = useState('');
const [transferId, setTransferId] = useState('');
const [visible, setVisible] = useState(false);
const [refundMessage, setRefundMessage] = useState('');
const [transactionId, setTransactionId] = useState('');
const [responseMessage, setResponseMessage] = useState('');

The code above handles the states for both the refunds and transfers. The transfer and refund code is written on the transferForm page. Next, you have to fetch the clientToken.

// Fetches the Braintree client token when the component mounts
useEffect(() => {
  const fetchClientToken = async () => {
    try {
      const response = await fetch("/api/braintreeConfig/braintreeConn");
      const data = await response.json();
      setClientToken(data.clientToken);
    } catch (error) {
      console.error("Error fetching Braintree client token:", error);
    }
  };

  fetchClientToken();
}, []);

The clientToken is fetched using the fetchClientToken function. It connects to the API folder where the clientToken is gotten from the Braintree API gateway. Next, You have to handle the transfer.

// Handles the payment transfer process using Braintree
const handleTransfer = async (nonce) => {
  try {
    const response = await fetch("/api/braintreeConfig/processTransfer", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ nonce, amount, recipientEmail }),
    });

    if (response.ok) {
      const data = await response.json();
      setTransferId(data.transactionId);
    } else {
      console.error(
        "Transfer failed. Please check your information and try again.",
      );
    }
  } catch (error) {
    console.error("Error initiating transfer:", error);
  }
};

The code above is used to send transfer amount and recipient email to the API folder that handles transfers. Also, a nonce is sent to the backend in the code above. The nonce is a secure token created randomly and uniquely. It stands for the number used once, hence the name nonce. Like its name, it is used once and created again if the user initiates another payment process.

The next part of the code is the tokenization.

// Tokenizes payment information using Braintree client SDK
const tokenizePaymentInfo = () => {
  return new Promise((resolve, reject) => {
    braintree.client.create(
      { authorization: clientToken },
      (clientErr, clientInstance) => {
        if (clientErr) {
          reject(clientErr);
          return;
        }

        clientInstance.request(
          {
            endpoint: "payment_methods/credit_cards",
            method: "post",
            data: {
              creditCard: {
                number: "4111111111111111",
                expirationDate: "10/23",
              },
            },
          },
          (tokenizeErr, payload) => {
            if (tokenizeErr) {
              console.error("Tokenization error:", tokenizeErr);
              return;
            }
            const nonce = payload.creditCards[0].nonce;
            handleTransfer(nonce);
          },
        );
      },
    );
  });
};

The code above is used to get the payment nonce, which is used to initiate transfers or payments. It is done using the clientToken for authorization and creating a nonce for the payment data or details.

After that, add the code for handling refunds.

// Handles the refund process using Braintree
const handleRefund = async () => {
  try {
    const response = await fetch("/api/braintreeConfig/paymentRefund", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ transactionId, amount }),
    });

    if (response.ok) {
      const data = await response.json();
      console.log("Transaction ID sent to the server successfully");
      setRefundMessage(data.refundId);
      console.log(data.refundId);
      setResponseMessage("Refund Successful!! Refund ID:");
    } else {
      const data = await response.json();
      console.error(
        "There was an error sending the transaction ID to the server",
        data.error,
      );
      setResponseMessage(data.error || "Refund failed");
    }
  } catch (error) {
    console.error(`Some form of error occured: ${error}`);
  }
};

The code gets the transactionId and amount for that transaction that will be set for refund.

Next, in the API folder, create the processTransfer.js file and add the following code:

// Handles the payment transfer process on the server side
export default async function handler(req, res) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  const { nonce, amount, recipientEmail } = req.body;
  try {
    const result = await gateway.transaction.sale({
      amount,
      paymentMethodNonce: nonce,
      options: { submitForSettlement: true },
      customer: { email: recipientEmail },
    });

    if (result.success) {
      res
        .status(200)
        .json({ success: true, transactionId: result.transaction.id });
    } else {
      res.status(500).json({ success: false, error: result.message });
    }
  } catch (error) {
    res.status(500).json({ success: false, error: "Internal server error" });
  }
}

The code above is for processing transfers. Next, create a file called paymentRefunds.js in the API folder and write the following code:

// Handles the refund process on the server side
export default async function handler(req, res) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  const { transactionId, amount } = req.body;
  // console.log(transactionId)
  // console.log(amount)

  try {
    const result = await gateway.transaction.refund(transactionId, amount);

    if (result.success) {
      console.log("Refund ID:", result.transaction.id);
      // Handle success, update database, send confirmation, etc.
      res.status(200).json({ success: true, refundId: result.transaction.id });
    } else {
      console.error("Refund failed:", result.message);
      // Handle failure, send error response, etc.
      res.status(400).json({ success: false, error: result.message });
    }
  } catch (error) {
    console.error("Error processing refund:", error);
    if (error.type === "notFoundError") {
      // Handle specific notFoundError
      res
        .status(404)
        .json({ success: false, error: "Transaction ID not found" });
    } else {
      // Handle other errors
      res.status(500).json({ success: false, error: "Internal server error" });
    }
  }
}

The code above processes refunds. If the transaction ID is invalid, it returns an error, and if the actual amount for that particular transaction is not input correctly, it also returns an error.

BT_sandbox14

The image above shows the refund and transfer forms. The GIF below shows how the transfer and refund works. 5c1d71be-5843-45d9-89ed-1ea37e285a8f-ezgif.com-video-to-gif-converter

Setting up Webhooks

That is all for making payments, transfers, and refunds, but you may wonder how the webhook works. In a system or when using an API gateway, they usually give you a webhook endpoint you can connect to, using your API endpoint to receive real-time notifications based on the events that occur while using that system. When you receive a notification, you can decide the logic you want to use for users to receive relevant information. Now, based on the Braintree API gateway, to set up a webhook, you have to do the following:

  • If you already have a sandbox account, log in to it.

  • Then, inside your sandbox account, click on the gear icon and select API from the dropdown, just like in the image below. BT_sandbox15

  • On the API page, click on webhooks and then click on create webhook. BT_sandbox4

  • In the webhook page, input the endpoint that will receive the webhook notifications and select the event notifications you want to receive. BT_sandbox16

  • Lastly, you click save to save the endpoint. BT_sandbox17

Note that to use Braintree webhook, your endpoint has to use HTTPS, not HTTP.

To ensure your development server runs on HTTPS, you can go to Ngrok.com, create an account, and download Ngrok onto your PC. After downloading it, install it, open a new terminal, and write the command:

Ngrok http 3000

You can replace 3000 with your port number and use the HTTPS URL they give you to run your localhost or test the webhook endpoint in Braintree. Now that Ngrok is live, we need to write the code to receive the webhook notifications from Braintree in our Next.js project.

To do so, you will have to use the API folder. Inside the app folder, create a file called webhookConn.js. Write the following code inside the file.

export default async function handler(req, res) {
  if (req.method === "POST") {
    try {
      // Parse the webhook notification
      const notification = req.body;
      // Add your custom logic to handle different webhook events
      let customResponseMessage;

      if (notification.kind === "TRANSACTION_CREATED") {
        console.log("Transaction successful from webhook");
        // Simulate a failed refund (for testing purposes)
        if (notification.transaction.status === "FAILED") {
          console.log("Refund failed. Handle accordingly.");
          customResponseMessage = "Refund failed!";
        } else {
          console.log("Refund successful. Handle accordingly.");
          customResponseMessage = "Refund processed successfully!";
        }
      }

      res
        .status(200)
        .json({ message: customResponseMessage, payload: notification });
    } catch (error) {
      console.error("Error processing Braintree webhook:", error);
      res.status(500).end("Internal Server Error");
    }
  } else if (req.method === "GET") {
    console.log("Received GET request");
    console.log("Environment Variables:", process.env);
    res.status(200).json({ message: "Hello from the GET endpoint!" });
  } else {
    res.setHeader("Allow", "POST, GET");
    res.status(405).end("Method Not Allowed");
  }
}

The code above listens and receives a webhook notification from Braintree. It handles a POST request from the Braintree webhook and GET when the browser uses the API. The following body of code logs to the user based on some notification.

if (notification.kind === "TRANSACTION_CREATED") {
  console.log("Transaction successful from webhook");
  // Simulate a failed refund (for testing purposes)
  if (notification.transaction.status === "FAILED") {
    console.log("Refund failed. Handle accordingly.");
    customResponseMessage = "Refund failed!";
  } else {
    console.log("Refund successful. Handle accordingly.");
    customResponseMessage = "Refund processed successfully!";
  }
}

In the code notice, the notification.kind specifies the event notification type.

Next, to test your webhook endpoint on your server, go back to your sandbox on the webhook page beside the webhook API endpoint you specified. Click on the ‘Check URL’ button, and it will test your API endpoint URL from your server. If it works, it will return a status code of 200. Else, it will return 500 if it fails.

An image of mine is shown below. BT_sandbox13

While on the terminal with my Ngrok running, I receive the POST notification and the status 200 with an OK response

ngrok1

Finally, webhooks are real-time notifications of events in your application or from your external API services. These services will provide a webhook to connect your server endpoint API to.

Conclusion

In conclusion, we looked at what webhooks are, their benefits, how to make payments, transfers, and refunds Using Braintree API, and how to use webhooks in Braintree. Here are all the links: Ngrok Braintree sandbox GitHub

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay