Building an Electricity VAS Integration with Node.js: Complete XpressPay Implementation
Backend • 5 min read

Building an Electricity VAS Integration with Node.js: Complete XpressPay Implementation

Julius Fasema

Author

Many applications that provide financial or utility services need to integrate with third-party Value-Added Service (VAS) providers

Introduction

Many applications that provide financial or utility services need to integrate with third-party Value-Added Service (VAS) providers.

For this project, I needed to integrate electricity bill payment into an existing Node.js application using XpressPay's VAS API.

Rather than implementing all the electricity provider logic independently, the application communicates with XpressPay, which provides the API layer for retrieving electricity products, validating customer information, and fulfilling electricity purchases.

I started with the electricity integration.

The implementation involved three major operations:

  1. Retrieve available electricity providers.

  2. Validate the customer's meter or account number.

  3. Initiate the electricity purchase.

The purchase operation also required HMAC SHA512 authentication using the XpressPay private key.

This article walks through the complete implementation.

1. Understanding the Integration

Before writing the Node.js code, I first went through the XpressPay documentation to identify the endpoints required for electricity.

The three endpoints used for the integration were:

Retrieve electricity products

/products?categoryId=3

The staging endpoint used during development was:

https://billerstest.xpresspayments.com:9603/api/v1/products?categoryId=3

The categoryId of 3 represents the electricity category used in this integration.

Validate electricity account

/electricity/validate

Staging:

https://billerstest.xpresspayments.com:9603/api/v1/electricity/validate

Fulfil electricity purchase

/electricity/fulfil

Staging:

https://billerstest.xpresspayments.com:9603/api/v1/electricity/fulfil


The general workflow therefore became:

2. Configuring the Environment

As with other third-party integrations, I did not hard-code the API credentials into the source code.

The XpressPay configuration was stored in the .env file.

For example:

XPRESSPAY_BASE_URL_STAGING=https://billerstest.xpresspayments.com:9603/api/v1

XPRESSPAY_PUBLIC_KEY=YOUR_PUBLIC_KEY

XPRESSPAY_PRIVATE_KEY=YOUR_PRIVATE_KEY


The Node.js application then reads these values using process.env.

The private key is particularly important because it is used to generate the HMAC SHA512 payment hash.

It should never be exposed to the frontend or committed to source control.

3. Installing Axios

Axios was used to make HTTP requests to the XpressPay API.

If Axios is not already installed:

npm install axios


The Node.js crypto module was also used.

Unlike Axios, crypto does not need to be installed because it is included with Node.js.

The modules used in the integration are:

const axios = require('axios');

const crypto = require('crypto');

const { Buffer } = require('buffer');


const {createAppLog} = require("../utils/createLog");

4. Complete Electricity.js Source Code

The following is the complete Electricity.js implementation used for the integration.

const axios = require('axios');

const crypto = require('crypto');

const {createAppLog} = require("../utils/createLog");


const base_url = process.env.XPRESSPAY_BASE_URL_STAGING;

const private_key = process.env.XPRESSPAY_PRIVATE_KEY;

const public_key = process.env.XPRESSPAY_PUBLIC_KEY;


// fetch billers type

const getElectricityBillers = async (serviceId) => {


    const base_url = process.env.XPRESSPAY_BASE_URL_STAGING;


    const apiUrl = `${base_url}/products?categoryId=${serviceId}`;


    const headers = {

        'Authorization': `Bearer ${public_key}`,

        'Content-Type': 'application/json',

    };


    const body = {

        size: 23,

        page: 0

    };


    await createAppLog(

        JSON.stringify({

            controller: "Electricity.js",

            action: "getProductServiceType",

            header: headers,

            url: apiUrl

        })

    );


    try {


        // Make the POST request using axios

        // and return the response data.

        const response = await axios.post(

            apiUrl,

            body,

            {

                headers: headers

            }

        );


        const transactionStatus =

            JSON.stringify(response.data);


        return transactionStatus;


    } catch (error) {


        // Handle errors here.

        await createAppLog(error);


        data = {

            status: error.response.data.status,

            message: error.response.data.message

        };


        return JSON.stringify(data);

    }

};


// verify meter number

const verifyMeterNumber = async (

    uniqueCode,

    accountNumber,

    accountType

) => {


    const apiUrl = `${base_url}/electricity/validate`;


    const headers = {

        'Authorization': `Bearer ${public_key}`,

        'Content-Type': 'application/json',

    };


    const postData = {

        requestId: "ignore",

        uniqueCode: uniqueCode,

        details: {

            accountNumber: accountNumber,

            accountType: accountType

        }

    };


    await createAppLog(

        JSON.stringify({

            controller: "Electricity.js",

            action: "verifyMeterNumber",

            parameter: postData,

            header: headers,

            url: apiUrl

        })

    );


    try {


        const response = await axios.post(

            apiUrl,

            postData,

            {

                headers

            }

        );


        const transactionData =

            JSON.stringify(response.data);


        return transactionData;


    } catch (error) {


        await createAppLog(error);


        const data = {

            status: error.response?.data?.status,

            message: error.response?.data?.message

        };


        return JSON.stringify(data);

    }

};


// purchase electricity

const billPayment = async (

    requestId,

    uniqueCode,

    accountNumber,

    accountType,

    amount,

    name,

    address,

    phoneNumber

) => {


    const apiUrl = `${base_url}/electricity/fulfil`;


    // Request body

    const postData = {

        requestId: requestId,

        uniqueCode: uniqueCode,

        details: {

            accountNumber: accountNumber,

            accountType: accountType,

            amount: amount,

            name: name,

            address: address,

            phoneNumber: phoneNumber

        }

    };


    // Generate HMAC SHA512 from

    // the EXACT JSON body

    const paymentHash = generatePaymentHash(postData);


    const headers = {

        'Authorization': `Bearer ${public_key}`,

        'PaymentHash': paymentHash,

        'Content-Type': 'application/json'

    };


    await createAppLog(

        JSON.stringify({

            controller: "Electricity.js",

            action: "BillPayment",

            parameter: postData,

            header: {

                ...headers,

                PaymentHash: "[HIDDEN]"

            },

            url: apiUrl

        })

    );


    try {


        const response = await axios.post(

            apiUrl,

            postData,

            {

                headers

            }

        );


        return JSON.stringify(response.data);


    } catch (error) {


        await createAppLog(error);


        const data = {

            status: error.response?.data?.status,

            message: error.response?.data?.message

        };


        return JSON.stringify(data);

    }

};



// Generate HMAC SHA512

const generatePaymentHash = (postData) => {


    const jsonBody = JSON.stringify(postData);


    return crypto

        .createHmac('sha512', private_key)

        .update(jsonBody)

        .digest('hex');

};


module.exports = {

    getElectricityBillers,

    verifyMeterNumber,

    billPayment,

};

5. Retrieving Electricity Providers

The first function is:

getElectricityBillers()

Its purpose is to retrieve the electricity products available through XpressPay.

The function accepts a serviceId:

const getElectricityBillers = async (serviceId) => {


The API URL is constructed dynamically:

const apiUrl =

    `${base_url}/products?categoryId=${serviceId}`;


For electricity:

categoryId=3


Therefore, calling:

getElectricityBillers(3)

produces a request to:

/products?categoryId=3

6. Adding the Authorization Header

The request uses the XpressPay public key as a bearer token:

const headers = {

    'Authorization': `Bearer ${public_key}`,

    'Content-Type': 'application/json',

};


The private key is not sent directly in this request.

The distinction between the public and private keys becomes especially important when processing the electricity purchase because the private key is used to generate the payment hash.

7. Passing Pagination Information

The request also contains:

const body = {

    size: 23,

    page: 0

};

This tells the API how many records to request and which page to retrieve.

The request is then sent using Axios:

const response = await axios.post(

    apiUrl,

    body,

    {

        headers: headers

    }

);

The API response is returned to the application:

const transactionStatus =

    JSON.stringify(response.data);


return transactionStatus;


The frontend can then use this information to display the available electricity providers.

8. Validating the Meter Number

After retrieving the electricity providers, the next operation is validating the customer's account.

This is handled by:

verifyMeterNumber()

The function accepts:

uniqueCode

accountNumber

accountType


The endpoint is:

const apiUrl =

    `${base_url}/electricity/validate`;


The request body is:

const postData = {

    requestId: "ignore",

    uniqueCode: uniqueCode,

    details: {

        accountNumber: accountNumber,

        accountType: accountType

    }

};


The uniqueCode identifies the selected electricity provider, while accountNumber contains the customer's meter or account number.

9. Sending the Validation Request

The request is sent with Axios:

const response = await axios.post(

    apiUrl,

    postData,

    {

        headers

    }

);


The response is then returned:

const transactionData =

    JSON.stringify(response.data);


return transactionData;


This gives the application an opportunity to confirm that the supplied electricity account information can be validated before proceeding with the purchase.

10. Initiating the Electricity Purchase

The final operation is:

billPayment()

It receives:

requestId

uniqueCode

accountNumber

accountType

amount

name

address

phoneNumber


The endpoint is:

const apiUrl =

    `${base_url}/electricity/fulfil`;


The request body is:

const postData = {

    requestId: requestId,

    uniqueCode: uniqueCode,

    details: {

        accountNumber: accountNumber,

        accountType: accountType,

        amount: amount,

        name: name,

        address: address,

        phoneNumber: phoneNumber

    }

};


At this point, the request contains the information required to process the electricity purchase.

But there is one more requirement.

The request needs a payment hash.

11. Generating the HMAC SHA512 Payment Hash

The payment hash is generated from the request body using HMAC SHA512.

The implementation is:

const generatePaymentHash = (postData) => {


    const jsonBody = JSON.stringify(postData);


    return crypto

        .createHmac('sha512', private_key)

        .update(jsonBody)

        .digest('hex');

};

First, the request body is converted into a JSON string:

const jsonBody =

    JSON.stringify(postData);

The HMAC is then created using SHA512 and the XpressPay private key:

crypto.createHmac(

    'sha512',

    private_key

)

The JSON body is supplied to the hash:

.update(jsonBody)


Finally, the result is converted to hexadecimal:

.digest('hex');

12. Adding the PaymentHash Header

The generated hash is then included in the request headers:

const paymentHash =

    generatePaymentHash(postData);

const headers = {

    'Authorization': `Bearer ${public_key}`,

    'PaymentHash': paymentHash,

    'Content-Type': 'application/json'

};


The request therefore contains two important authentication-related values:

This is an important distinction.

The private key is used to generate the signature, rather than being sent directly to the external API.

13. Why the Exact JSON Body Matters

One of the most important details in this implementation is this comment:

// Generate HMAC SHA512 from the EXACT JSON body

The hash is generated from:

JSON.stringify(postData)

The same postData object is then sent to the API:

axios.post(

    apiUrl,

    postData,

    {

        headers

    }

);


Conceptually:

When an API uses request-body signing, the data used to generate the signature must correspond to the data being submitted.

This is one of the areas where small differences in implementation can cause authentication failures.

14. Logging the Payment Request

Because this is a payment operation, logging is important for troubleshooting.

However, the generated payment hash should not be stored in plain form in application logs.

That is why the implementation uses:

await createAppLog(

    JSON.stringify({

        controller: "Electricity.js",

        action: "BillPayment",

        parameter: postData,

        header: {

            ...headers,

            PaymentHash: "[HIDDEN]"

        },

        url: apiUrl

    })

);

Instead of logging the actual hash, the value is replaced with:

[HIDDEN]

This provides useful information during debugging without unnecessarily exposing authentication material.

15. Handling Errors

Each major API operation uses try...catch.

For example:

try {


    const response = await axios.post(

        apiUrl,

        postData,

        {

            headers

        }

    );


    return JSON.stringify(response.data);


} catch (error) {


    await createAppLog(error);


    const data = {

        status: error.response?.data?.status,

        message: error.response?.data?.message

    };


    return JSON.stringify(data);

}


The optional chaining operator:

error.response?.data?.status


is useful because an Axios error does not always contain a response.

For example, a network failure may occur before the external server returns an HTTP response.

Without optional chaining, the error-handling code itself could throw another exception.

16. Keeping the Integration Modular

The three operations are kept as separate functions:

getElectricityBillers

verifyMeterNumber

billPayment

They are then exported:

module.exports = {

    getElectricityBillers,

    verifyMeterNumber,

    billPayment,

};


This allows another part of the Node.js application to import only the functions it needs.

For example:

const {

    getElectricityBillers,

    verifyMeterNumber,

    billPayment

} = require('./Electricity');


Each service can then contain the provider-specific implementation while the rest of the application remains relatively independent of the external API.

17. The Complete Electricity Flow

Putting everything together, the application follows this sequence:

                  

This represents the complete flow implemented around the XpressPay electricity API.

18. Lessons From the Integration

One of the things I found while implementing the integration was that the API calls themselves were not the most difficult part.

The more important work was understanding how the individual API operations fit together.

The application needed to:

  • Identify the correct electricity category.

  • Retrieve available electricity providers.

  • Keep the provider's uniqueCode.

  • Validate the customer's account.

  • Build the correct payment request.

  • Generate the HMAC SHA512 payment hash.

  • Send the correct authentication headers.

  • Handle API errors.

  • Log useful debugging information.

  • Prevent sensitive authentication information from appearing in logs.

The integration therefore became more than a simple HTTP client.

It became an integration layer between the application's payment workflow and the external VAS provider.

Conclusion

Building the electricity VAS integration with Node.js provided a practical example of integrating a third-party utility API into an existing application.

The implementation started by retrieving electricity providers using:

/products?categoryId=3

The selected electricity account was then validated using:

/electricity/validate

Finally, the electricity purchase was initiated through:

/electricity/fulfil

The final step required HMAC SHA512 authentication, where the request body was hashed using the XpressPay private key and the resulting hash was supplied through the PaymentHash header.

Node.js made the implementation straightforward through two important tools:

  • Axios for communicating with the XpressPay API.

  • Crypto for generating the HMAC SHA512 payment hash.

The complete implementation also demonstrates several important backend development practices, including environment-based configuration, modular services, API error handling, logging, and protecting sensitive authentication information.

With the electricity integration working, the same architecture can be extended to other VAS services such as airtime, data, cable TV, and other bill-payment services.

Technologies Used

  • Node.js

  • JavaScript

  • Axios

  • XpressPay VAS API

  • REST API

  • HMAC SHA512

  • Node.js Crypto

  • Environment Variables

  • JSON

  • Electricity Bill Payment API

  • Backend API Integration

Tags: Node.js, JavaScript, XpressPay, VAS, Electricity API, Axios, HMAC SHA512, REST API, Payment Integration, API Integration, Backend Development