DEVELOPER

Back to Developer Blog

technicalseries

Process Payments and Securely Accept Refunds in your Node.js App

By Kevin Kimani and Laura Olson | January 28th, 2025

North offers a range of products—from APIs and SDKs to plugins—that you can combine in various ways to help you fine-tune your app's payment processing.

In this article, you'll learn how to integrate two of these APIs—the Browser Post API and the Custom Pay API—for a highly secure and flexible payment solution.

Checkout form made with North's Browser Post API

Build this App

Clone this code repository to quickly create your own app today!

Browser Post API and Custom Pay API Overview

Before we get to the tutorial, let's get an overview of these APIs and how you'll be using them.

The Browser Post API is an HTTP platform that allows you to set up a payment form on your website to securely send the customer's card information directly to the servers of North's in-house processor, EPX. Once the financial processing is completed, the customer's browser is redirected back to the merchant's site along with the results of the financial operation.

The major benefit of the Browser Post API is that it keeps the cardholder's data out of your environment, simplifying PCI compliance and helping prevent security compromises.

In this article, you'll learn how to combine the security benefits of the Browser Post API with the advanced functionality of the Custom Pay API. The Custom Pay API provides several API endpoints that make it easy to customize the flow of payment transactions according to your specific requirements. In addition to authorizing and capturing payments, these endpoints let you handle tasks such as issuing refunds, making tip adjustments, closing batches, and accessing information about previous transactions.

In the following sections, you will build a Node.js web application that uses the Browser Post API to tokenize, securely authorize, and capture a payment. You will then use the Custom Pay API to complete a refund.

The benefit of using both APIs is to make a sale with the Browser Post API—which, unlike the Custom Pay API, keeps credit card data out of the merchant's systems—and then pass the returned BRIC transaction token to one of the Custom Pay API's endpoints to perform additional actions such as making refunds.

Refund button made with Custom Pay API

Project Overview

Here is an overview of the Node.js app you'll build in this tutorial. You'll create a web page with a button that a customer can click to indicate that they are ready to check out. The app will make a terminal authorization code (TAC) request to the EPX servers and load the payment form.

The customer will then fill out the payment form that will post the data directly to the EPX servers for processing, bypassing your system completely. This will be a CCE1 transaction type (ecommerce purchase authorization and capture transaction). You can refer to the EPX Data Dictionary in the Supplemental Resources folder for more information on transaction types.

Once the payment is processed and the results are posted to your redirect URL, you will display a payment results page that shows whether the transaction was authorized and captured successfully. All of this is done using the Browser Post API.

On the payment results page you will also display a refund button. Clicking this button will use the Custom Pay API's /refund/{BRIC} endpoint to return the funds to the user’s account and show the results in the browser.

Get in Touch

Talk to us about accepting credit card payments and refunds on your website today.

Prerequisites

To follow along with this tutorial, you'll need the following:

You also need to contact the North Integration team for test credentials. Make sure you provide a REDIRECT_URL when requesting them. The REDIRECT_URL is where the Browser Post API payment results are posted after processing.

Lastly, create a root folder for your project called browser-post-custom-pay-apis and open it in your terminal.

Setting Up the Server

In this section, you will set up the server that will be used to serve the EJS templates, request a TAC from the EPX servers, and implement the logic to capture and refund a transaction.

First, you'll set up the server logic. This is where you will integrate the Custom Pay API to refund the transaction that's been authorized and captured using the Browser Post API.

With the browser-post-custom-pay-apis folder open in your terminal, execute the command below to initialize a Node.js project:

npm init -y

Next, open the package.json file and replace the scripts key-value pair with the following:

"scripts": {
   "start": "node server"
 },

This script starts the Node server.

Next, execute the command below to install all the dependencies required for this project:

npm install express ejs cors dotenv form-urlencoded querystring jsdom node-fetch@2

Here is how each of these dependencies will be used:

  • express: to set up a Node.js server
  • ejs: to create HTML templates
  • cors: to enable cross-origin resource sharing
  • dotenv: to load environment variables from an .env file
  • form-urlencoded: to create x-www-form-urlencoded string data
  • querystring: to parse the HTML form data received from the Browser Post API
  • jsdom: to traverse the XML tree returned from the North key exchange service and extract the TAC
  • node-fetch@2: to make API requests to the EPX servers

Next, in the project root folder, create a file named server.js, which will be used as the entry point into the Node server and implement the server logic. Paste the code below into it:


// import the required dependencies
const express = require('express')
require('dotenv').config()
const fetch =  require('node-fetch')
var cors = require('cors')
var formurlencoded = require('form-urlencoded')
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const querystring = require('querystring');
const crypto = require('crypto')

// create an instance of the Express app
const app = express()

// Allow CORS
app.use(cors({
 origin: '*',
}))

// configure middleware for parsing incoming request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// set the view engine to ejs
app.set('view engine', 'ejs');

// set the server port
const port = 3000

// define a variable to hold payment result data
let paymentResult

// serve the homepage
app.get('/', (req, res) => {
 res.render('home')
})

// make a TAC request to the EPX servers
app.post('/getTAC', async (req, res) => {
 const { amount } = req.body

 const MAC = process.env.MAC
 const TRAN_NBR = Math.floor(Math.random() * 1000000000)
 const TRAN_GROUP = process.env.TRAN_GROUP
 const REDIRECT_URL = process.env.REDIRECT_URL

 const formData = {
   AMOUNT: amount,
   MAC,
   TRAN_NBR,
   TRAN_GROUP,
   REDIRECT_URL
 }

 try {
   const response = await fetch('https://keyexch.epxuap.com', {
       method: 'post',
       body: formurlencoded(formData),
       headers: {'Content-Type': 'application/x-www-form-urlencoded'}
   });

   const data = await response.text()

   const dom = new JSDOM(data)
   const TAC = dom.window.document.querySelector("FIELD").textContent

   res.status(200).json({
       TAC: TAC
   })

 } catch (error) {
   console.log(error)
   res.status(400).json({
       error: 'An error occurred.'
   })
 }
})

// serve the payment form
app.get('/paymentForm', (req, res) => {
 const tran_code = process.env.TRAN_CODE
 const cust_nbr = process.env.CUST_NBR
 const merch_nbr = process.env.MERCH_NBR
 const dba_nbr = process.env.DBA_NBR
 const terminal_nbr = process.env.TERMINAL_NBR
 const industry_type = process.env.INDUSTRY_TYPE

 res.render('paymentForm', {
   tran_code,
   cust_nbr,
   merch_nbr,
   dba_nbr,
   terminal_nbr,
   industry_type
 })
})

// receive the Browser Post API transaction results
app.post('/paymentResult', async (req, res) => {
 try {
   const result = req.body;

   const data = querystring.stringify(result);
   const dataObj = JSON.parse('{"' + decodeURI(data.replace(/&/g, "\",\"").replace(/=/g,"\":\"")) + '"}')

   paymentResult = dataObj

   res.render('result', {
     status: paymentResult.AUTH_RESP_TEXT
   })

   console.log('payment result', dataObj)

 } catch (error) {
   console.log('error:', error);

   res.status(400).json({
     data: 'Payment failed.'
   });
 }
});

// refund the payment payment using the Custom Pay API
app.get('/refundPayment', async (req, res) => {
 try {
     payload = {
       amount: Number(paymentResult.AUTH_AMOUNT),
       transaction: 123,
       batchID: 10000,
       industryType: 'E',
       cardEntryMethod: 'X'
     }

     const concat_payload = `/refund/${paymentResult.AUTH_GUID}` + JSON.stringify(payload)

     // generate the ePISignature following the instructions in the "How To Authenticate" section of the Custom Pay API Integration Guide

     headers={
         'BRIC': paymentResult.AUTH_GUID,
         'EPI-Id': process.env.EPI_ID,
         'EPI-Signature': epi_signature,
         'Content-Type': 'application/json',
         'EPI-Trace': 'test213'
     }

     try {
       const response = await fetch(`https://epi.epxuap.com/refund/${paymentResult.AUTH_GUID}`, {
           method: 'POST',
           body: JSON.stringify(payload),
           headers: headers
       });
  
       const data = await response.json()

       console.log('refund result', data)
  
       res.render('refundResult', {
           refundStatus: data.data.text
       })
  
     } catch (error) {
       console.log(error)
       res.status(400).json({
           error: 'An error occurred.'
       })
     }
 } catch (error) {
   console.log('error:', error);

   res.status(400).json({
     data: 'Refund failed.'
   });
 }
})

app.listen(port, () => {
 console.log(`Example app listening on port ${port}`)
})

The code above imports the required dependencies, applies some configurations (commented in the code) to your Express application, and defines a paymentResult variable that will be used to hold the payment results received from the Browser Post API. It then defines five API endpoints:

  • The GET / endpoint is used to render the homepage template, which you'll create later in the tutorial.
  • The POST /getTAC endpoint is used to make a TAC request to the EPX Sandbox Key Exchange service. When making the TAC request, North recommends setting the Content-Type header value to application/x-www-form-urlencoded. The required fields for this request are AMOUNT, MAC, TRAN_NBR, TRAN_GROUP, and REDIRECT_URL. The MAC value is provided by the Integrations team, AMOUNT represents the total amount for the transaction, TRAN_NBR represents the transaction number in the current batch, TRAN_GROUP validates that the TRAN_CODE submitted during the transaction corresponds to a general category of payment types, and the REDIRECT_URL takes in the value of the redirect URL that you provided to the Integrations team. For this transaction, you will use SALE as the value for the TRAN_GROUP field. It indicates that this is an authorization and capture transaction. This request returns a response in XML format, and the code uses the jsdom package to traverse the XML tree and extract the TAC value.
  • The GET /paymentForm endpoint retrieves some environment variable values and renders the paymentForm template with some data. You will learn more about these values when working on the EJS templates in the next section.
  • The POST /paymentResult endpoint receives the Browser Post API payment results from the EPX servers. The result is an HTML form. The code uses the querystring package to extract the form fields and their corresponding values and convert the result to a JSON object using JSON.parse('{"' + decodeURI(data.replace(/&/g, "","").replace(/=/g,"":"")) + '"}'). The object is then assigned to the paymentResult variable, and the route renders the result template that shows and passes the required transaction data.
  • The GET /refundPayment endpoint refunds a payment to the customer’s account. To make a refund using the Custom Pay API, you make a POST request to /refund/{BRIC} endpoint. This endpoint allows you to make a refund to a previously authorized and captured transaction using its BRIC transaction token. The BRIC token, also referred to as the GUID token, is a globally unique identifier that is used to represent a transaction performed on the EPX servers.

The GET /refundPayment endpoint endpoint takes in several headers:

  • EPI-Id is provided by the North integrations team.
  • EPI-Signature is generated by following the instructions in the "How To Authenticate" section of the Custom Pay API Integration Guide.
  • Content-Type is the content type of the message.
  • EPI-Trace is a unique value defined by the merchant that's used to identify a transaction. This value is not persisted with the transaction and is only required in the current request/response chain.
  • BRIC is the unique token generated for each transaction.

The request body for this endpoint takes in the following values:

  • The amount is a positive dollar amount for the funds to be transferred during the transaction.
  • The transaction is a unique code defined by the merchant to identify a transaction.
  • The batchID is a unique number defined by the merchant to identify a batch of transactions.
  • The industryType is the industry to which the transaction belongs. You can refer to the EPX Data Dictionary located in the Supplemental Resources folder for the values allowed or check out the Custom Pay API specification.
  • The cardEntryMethod indicates how the card number for the transaction was entered. You can refer to the EPX Data Dictionary located in the Supplemental Resources folder for the values or check out the Custom Pay API specification.

The GET /refundPayment endpoint defines a payload object containing all the fields required for the request body. The endpoint for the transaction is then concatenated with the payload, and the result is stored in a variable named concat_payload. Next, add the code to generate the EPI-Signature following the instructions in the "How To Authenticate" section of the Custom Pay API Integration Guide.

Lastly, it defines a headers variable with all the values required for the request headers and makes a POST request to the Custom Pay API /refund/BRIC endpoint to make a refund. Once the transaction is processed and the result is received, it renders the refundResult template and passes the required data.

Setting Up the EJS Templates

With the server logic ready, you will now implement the EJS templates that you will use to interact with the system. This is where you will integrate the Browser Post API to authorize a transaction.

In the project root directory, create a new folder named views and a new file in it named home.ejs. Add the code below to this file:

HTML
<!DOCTYPE html>
<html lang="en">
   <head>
   	<meta charset="UTF-8" />
   	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
   	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
   	<title>North Developer</title>
   	<style>
       	body {
           	height: 100vh;
           	width: 100vw;
           	overflow: hidden;
           	display: flex;
           	justify-content: center;
           	margin-top: 200px;
           	background-color: rgb(241, 228, 228);
       	}

       	div {
           	background-color: white;
           	height: fit-content;
           	width: 400px;
           	padding: 2rem;
           	border-radius: 10px;
       	}

       	button {
           	display: inline-block;
           	padding: 10px 20px;
           	background-color: #3498db;
           	color: #fff;
           	font-size: 16px;
           	font-weight: bold;
           	text-align: center;
           	text-decoration: none;
           	border-radius: 4px;
           	border: none;
           	cursor: pointer;
       	}
   	</style>
   </head>
   <body>
   	<div>
       	<h3>Pay Now</h3>
       	<button id="payment-button">Pay $11.99</button>
   	</div>

   	<script>
       	const paymentBtn = document.getElementById("payment-button");

       	paymentBtn.addEventListener("click", () => {
           	fetch("/getTAC", {
               	method: "POST",
               	headers: {
                   	"Content-Type": "application/json",
               	},
               	body: JSON.stringify({ amount: 11.99 }),
           	})
               	.then((response) => response.json())
               	.then((data) => {
                   	if (data.TAC) {
                       	localStorage.setItem(
                           	"TAC",
                           	JSON.stringify(data.TAC)
                       	);
                       	localStorage.setItem("productPrice", 11.99);
                       	window.location.assign("/paymentForm");
                   	}
               	})
               	.catch((error) => {
                   	console.error("Error:", error);
               	});
       	});
   	</script>
   </body>
</html>

The code above renders a Pay $11.99 button. When a user clicks the button, it makes a POST request to the /getTAC API route with the hard-coded amount variable that represents the total transaction amount. When the API returns the response, this template saves the TAC and the transaction amount to the local storage and redirects the user to the /paymentForm page.

Next, in the views folder, create a file named paymentForm.ejs and add the code below to it:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>North Developer</title>
   <style>
       body {
           height: 100vh;
           width: 100vw;
           overflow: hidden;
           display: flex;
           justify-content: center;
           margin-top: 200px;
           background-color: rgb(241, 228, 228);
       }

       form {
           background-color: white;
           height: fit-content;
           width: 400px;
           padding: 2rem;
           border-radius: 10px;
       }

       .paymentFields {
           display: flex;
           flex-direction: column;
       }

       .paymentFields > div {
           margin-bottom: 10px;
       }

       .paymentFields label {
           display: block;
           font-weight: bold;
           margin-bottom: 5px;
       }

       .paymentFields input[type="number"] {
           padding: 5px;
           border-radius: 4px;
           border: 1px solid #ccc;
           font-size: 14px;
       }

       .paymentFields input[type="number"]:focus {
           outline: none;
           border-color: #3498db;
       }

       button {
           display: inline-block;
           padding: 10px 20px;
           background-color: #3498db;
           color: #fff;
           font-size: 16px;
           font-weight: bold;
           text-align: center;
           text-decoration: none;
           border-radius: 4px;
           border: none;
           cursor: pointer;
       }
   </style>
</head>
<body>
   <form action="https://services.epxuap.com/browserpost/" method="post">
       <h2>
           Payment Form
       </h2>
       <div class="paymentFields">
           <div>
               <label>Account Number</label>
               <input type="text" name="ACCOUNT_NBR" placeholder="Enter Account Number" value="4000520000000009">
           </div>
  
           <div>
               <label>Expiry Date</label>
               <input type="text" name="EXP_DATE" placeholder="YYMM" value="2504">
           </div>
  
           <div>
               <label>CVV</label>
               <input type="text" name="CVV2" placeholder="YYMM" value="123">
           </div>
       </div>

       <div style="display: none;">
           <input type="text" name="TRAN_CODE" value=<%=tran_code%>>
           <input type="text" name="CUST_NBR" value=<%=cust_nbr%>>
           <input type="text" name="MERCH_NBR" value=<%=merch_nbr%>>
           <input type="text" name="DBA_NBR" value=<%=dba_nbr%>>
           <input type="text" name="TERMINAL_NBR" value=<%=terminal_nbr%>>
           <input type="text" name="INDUSTRY_TYPE" value=<%=industry_type%>>
           <input type="text" name="TAC" id="tac">
           <input type="text" name="AMOUNT" id="price">
       </div>

       <button type="submit">Submit</button>
   </form>

   <script>
       document.addEventListener('DOMContentLoaded', () => {
           const TAC = JSON.parse(localStorage.getItem('TAC'))
           const productPrice = localStorage.getItem('productPrice')
           document.getElementById('tac').value = TAC
           document.getElementById('price').value = productPrice
       })
   </script>
</body>
</html>

This code renders a form that collects the payment data from the user and POSTs it directly to the EPX servers (Sandbox Payment URL). The following are the required fields for this form:

  • ACCOUNT_NBR: the customer's credit card number
  • EXP_DATE: the credit card's expiry date
  • CVV2: the card's security code
  • TRAN_CODE: identifies the type of transaction to be processed and is related to the TRAN_GROUP field you used earlier. For this transaction, you will use SALE as the value for the TRAN_CODE field, which indicates that this is an authorization and capture transaction.
  • CUST_NBR, MERCH_NBR, DBA_NBR, and TERMINAL_NBR: provided by the North Integrations team
  • INDUSTRY_TYPE: identifies the industry type of the transaction
  • TAC: received after sending a TAC request to the EPX servers
  • AMOUNT: total amount for the transaction

The code above displays only the account number, CVV, and expiry date fields and hides the rest since they do not need to be displayed to the user.

Next, create a result.ejs file in the views folder and add the code below to it:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>North Developer</title>
   <style>
       body {
           height: 100vh;
           width: 100vw;
           overflow: hidden;
           display: flex;
           justify-content: center;
           margin-top: 200px;
           background-color: rgb(241, 228, 228);
       }

       .container {
           background-color: white;
           height: fit-content;
           width: 400px;
           padding: 2rem;
           border-radius: 10px;
           display: flex;
           flex-direction: column;
           align-items: center;
       }

       .check {
           width: 100px;
           height: 100px;
       }

       button {
           display: inline-block;
           padding: 10px 20px;
           background-color: #da1d04;
           color: #fff;
           font-size: 16px;
           font-weight: bold;
           text-align: center;
           text-decoration: none;
           border-radius: 4px;
           border: none;
           cursor: pointer;
           margin-top: 2rem;
       }
   </style>
</head>
<body>
   <div class="container">
       <h2>Payment Status</h2>
       <p><%=status%></p>
       <div class="check">
           <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
       </div>
       <button  id="refund-button">
           Request Refund
       </button>         
   </div>

   <script>
       const refundBtn = document.getElementById('refund-button')

       refundBtn.addEventListener('click', () => {
           window.location.assign('/refundPayment')
       })
   </script>
</body>
</html>

The code above displays the results of the transaction made using the Browser Post API. It also displays a Request Refund button that makes a GET request to the /refundPayment endpoint.

Next, create a refundResult.ejs file in the views folder and add the code below to it:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>North Developer</title>
   <style>
       		@font-face {
			font-family: 'Figtree';
			src: url('./assets/Figtree-VariableFont_wght.ttf') format('truetype');
			font-weight: 700;
			font-style: normal;
		}

       	body {
			margin: auto;
           	height: 100vh;
           	width: 100vw;
           	overflow: hidden;
           	margin-top: 200px;
           	background-color: #222529;
			font-family: 'Figtree', sans-serif;
			text-align: center;
       	}

		h1, h2 {
			text-align: center;
			color: #9A9FA2;
		}

		h1 {
			margin-top: 2rem;
			color: white;
		}

       	.paymentForm {
           	background-color: white;
           	height: fit-content;
           	width: 200px;
           	padding: 2rem;
           	border-radius: 10px;
			margin-top: 1rem;
			text-align: center;
			margin: 50px auto;
       	}

       	button {
           	display: inline-block;
           	padding: 10px 20px;
           	background-color: #1FC4B3;
           	color: #fff;
           	font-size: 16px;
           	font-weight: bold;
           	text-align: center;
           	text-decoration: none;
           	border-radius: 4px;
           	border: none;
           	cursor: pointer;
       	}

       .container {
           background-color: white;
           height: fit-content;
           width: 400px;
           padding: 2rem;
           border-radius: 10px;
           display: flex;
           flex-direction: column;
           align-items: center;
       }

       .check {
           width: 100px;
           height: 100px;
       }
   </style>
</head>
<body>
   <div class="container">
       <h2>Refund Status</h2>
       <p><%=refundStatus%></p>
       <div class="check">
           <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
       </div>        
   </div>
</body>
</html>

The code above displays the results of the refund made using the Custom Pay API.

Testing the Application

Now that the application is fully set up, you can deploy it and test whether it works as expected. Upload your application code to GitHub and deploy it on Render .

When deploying your application, make sure to set yarn start as the Start Command and to configure your environment variables . The environment variables that you should set include MAC, TRAN_GROUP, REDIRECT_URL, TRAN_CODE, CUST_NBR, MERCH_NBR, DBA_NBR, TERMINAL_NBR, INDUSTRY_TYPE, EPI_KEY, and EPI_ID. The values for these fields have been discussed throughout the article.

To test the app, open the deployed app's link provided by Render. If you click the "Pay Now" button, you should be navigated to a web page that renders a payment form which has been prefilled with the test details provided by North. If you click Submit, you should be navigated to a page that shows the results of the transaction and a button to request a refund. If you click the Request Refund button, you will be navigated to the refund results page.

How To Get Started

In this guide, you have learned about how you can use North's Browser Post API and Custom Pay API to fine-tune payment processing for your Node.js web application. You have learned how to use the Browser Post API to securely send card details to the EPX servers and the Custom Pay API to make a refund.

The combination of these solutions lets you keep sensitive cardholder data out of your server environment while benefitting from the advanced functionality provided by the Custom Pay API. Contact us to learn more about how these tools can meet your business needs.


Start your free Developer account and try it now.


©2025 North is a registered DBA of NorthAB, LLC. All rights reserved. North is a registered ISO of BMO Harris Bank N.A., Chicago, IL, Citizens Bank N.A., Providence, RI, The Bancorp Bank, Philadelphia, PA, FFB Bank, Fresno, CA, Wells Fargo Bank, N.A., Concord, CA, and PNC Bank, N.A.