Integrating VAS Services for Airtime and Data with Node.js
Backend • 5 min read

Integrating VAS Services for Airtime and Data with Node.js

Julius Fasema

Author

Learn how to integrate VAS services for airtime and data using Node.js, Axios, REST APIs, environment variables, and HMAC SHA512 authentication.

Integrating a Value-Added Service (VAS) provider into an application can look complicated at first, especially when the provider offers several services and requires API keys, authentication, request signatures, and specific request and response formats.

Recently, I was contracted to integrate XpressPay VAS services for airtime and data purchases. The goal was to build a backend service that could communicate with XpressPay, retrieve available data products, and process data and airtime purchases.

In this article, I will walk through the approach I used, from studying the API documentation to implementing the integration with Node.js and Axios.

Why Node.js?

One of the first decisions I had to make was which programming language to use for the integration.

After considering the requirements, I settled on Node.js.

Node.js is a good fit for this type of application because the integration involves making HTTP requests to an external service and waiting for responses. Node.js provides asynchronous, non-blocking I/O, which makes it convenient for applications that communicate with external APIs.

Another reason was simplicity. The integration mainly involved HTTP requests, JSON payloads, authentication headers, hashing, and response processing. Node.js provides the necessary tools to handle all of these without introducing unnecessary complexity.

For HTTP communication, I decided to use Axios.

Studying the XpressPay VAS Documentation

After deciding on the technology, the next step was to study the XpressPay VAS documentation.

Before writing any code, I needed to understand how the API worked and what information was required for each operation.

The documentation helped me identify:

  • The API base URL.
  • Authentication requirements.
  • Available endpoints.
  • HTTP methods.
  • Request payload structures.
  • Response structures.
  • Required headers.
  • Product and category identifiers.
  • Transaction identifiers.
  • Hash/signature requirements.
  • Error responses.
  • Staging and production environments.

Understanding the request and response payloads was particularly important because it allowed me to reproduce the requests in Postman before implementing them in Node.js.

This gave me a way to test the API independently and verify that the credentials, request parameters, headers, and endpoints were correct.

Understanding the Integration Flow

After studying the documentation, I identified three major operations required for the integration:

  1. Listing available data products
  2. Purchasing data
  3. Purchasing airtime

The basic flow was:

The Node.js application acts as the integration layer between the main application and XpressPay.

The application can request available products or initiate a purchase through the Node.js backend. The backend is then responsible for constructing the appropriate request, generating the required hash, communicating with XpressPay, and returning the response.

This also means that sensitive credentials such as the private key do not need to be exposed to the frontend.

Setting Up the Node.js Project

After understanding the API flow, I scaffolded the Node.js project and installed the packages required for the integration.

The main external package I needed was Axios.

npm install axios

Node.js also provides the crypto module as part of its standard library, so there was no need to install another package for generating the HMAC SHA512 hash.

The project was organized to keep the VAS integration logic separate from other application logic.

For example:

The exact structure can vary depending on the application, but separating external service integrations makes the application easier to maintain.

Configuring the API Credentials

As usual, the API credentials were configured using environment variables rather than being hard-coded into the source code.

For example:

XPRESSPAY_BASE_URL_STAGING=https://example-api-url

XPRESSPAY_PUBLIC_KEY=your_public_key

XPRESSPAY_PRIVATE_KEY=your_private_key

The Node.js application can then read these values using process.env.

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;

This approach makes it easier to use different configurations for development, staging, and production.

It also prevents sensitive credentials from being directly embedded in the source code.

The .env file should also be excluded from source control by adding it to .gitignore.

Listing Available Data Products

The first operation I implemented was retrieving the available data products.

The API provided a products endpoint that accepted a category and biller identifier. In this case, the category Id and biller Id will be supply by the caller service

The function was implemented as follows:

const dataProductList = async (categoryId, billerId) => {

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

    const headers = {

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

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

    };

    const body = {

        size: 155,

        page: 0

    };

    await createAppLog(

        JSON.stringify({

            controller: "Utility.js",

            action: "GetVtuNetworkDataList",

            header: headers,

            url: apiUrl

        })

    );

    try {

        const response = await axios.post(

            apiUrl,

            body,

            {

                headers: headers

            }

        );

        const transactionStatus =

            JSON.stringify(response.data);

        return transactionStatus;

    } catch (error) {

        await createAppLog(error);

        const data = {

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

            message: error.response?.data?.message

        };

        return JSON.stringify(data);

    }

};

The API URL is constructed from the configured base URL:

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

The request also contains an authorization header:

const headers = {

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

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

};

The request body contains pagination information:

const body = {

    size: 155,

    page: 0

};

Axios is then used to send the request:

const response = await axios.post(

    apiUrl,

    body,

    {

        headers: headers

    }

);

The response returned by XpressPay contains the available products that can be presented to the application or customer.

These products can then be used when initiating a data purchase.

Purchasing Data

After retrieving the available data products, the next step was to implement the data purchase operation.

The data fulfilment endpoint was:

/data/fulfil

The purchase request required information such as:

  • Request ID
  • Product unique code
  • Customer phone number
  • Amount

The request body was constructed as follows:

const postData = {

    requestId: requestId,

    uniqueCode: uniqueCode,

    details: {

        phoneNumber: phoneNumber,

        amount: amount

    }

};

The uniqueCode is important because it identifies the data product being purchased.

For example, after retrieving the list of available products, the application may allow the customer to select a particular data plan. The corresponding product code can then be supplied during the purchase.

Generating the Payment Hash

One of the important requirements for the purchase operation was generating a PaymentHash using HMAC SHA512.

I created a reusable function for this:

const generatePaymentHash = (postData) => {

    const jsonBody = JSON.stringify(postData);

    return crypto

        .createHmac('sha512', private_key)

        .update(jsonBody)

        .digest('hex');

};

The first step is converting the request body into JSON:

const jsonBody = JSON.stringify(postData);

The JSON string is then passed into the HMAC SHA512 process using the private key:

crypto

    .createHmac('sha512', private_key)

    .update(jsonBody)

Finally, the hash is returned as a hexadecimal string:

.digest('hex');

The generated hash is then included in the request headers.

Data Purchase Request

The complete data purchase function looked like this:

const dataBillPayment = async (

    requestId,

    uniqueCode,

    phoneNumber,

    amount

) => {

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

    const postData = {

        requestId: requestId,

        uniqueCode: uniqueCode,

        details: {

            phoneNumber: phoneNumber,

            amount: amount

        }

    };

    const paymentHash = generatePaymentHash(postData);

    const headers = {

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

        'PaymentHash': paymentHash,

        'channel': 'web',

        'Content-Type': 'application/json'

    };

    await createAppLog(

        JSON.stringify({

            controller: "Utility.js",

            action: "AirtimeDataBillPayment",

            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);

    }

};

The request is sent to:

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

The request body is then created:

const postData = {

    requestId: requestId,

    uniqueCode: uniqueCode,

    details: {

        phoneNumber: phoneNumber,

        amount: amount

    }

};

The PaymentHash is generated from that exact request body:

const paymentHash = generatePaymentHash(postData);

The hash is then added to the request headers:

const headers = {

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

    'PaymentHash': paymentHash,

    'channel': 'web',

    'Content-Type': 'application/json'

};

Finally, Axios sends the request:

const response = await axios.post(

    apiUrl,

    postData,

    { headers }

);

The response from XpressPay is then returned to the application.

Why the Exact JSON Body Matters

One important lesson from working with APIs that use HMAC signatures is that the data used to generate the signature matters.

In this implementation, the hash is generated from:

generatePaymentHash(postData);

The same postData object is then sent to the API.

This is important because the receiving server can independently generate the hash from the request body and compare it with the PaymentHash supplied by the client.

If the values used to generate the hash are different from the actual request, the generated hashes may not match.

Therefore, when implementing HMAC-based authentication, pay attention to:

  • Request property names.
  • Request property values.
  • JSON structure.
  • Data types.
  • Serialization.
  • Secret/private key.
  • Hash algorithm.
  • Output format.

A small difference in the data being signed can cause the API to reject the request.

Purchasing Airtime

After implementing the data purchase flow, I moved to the airtime purchase operation.

The airtime fulfilment endpoint was:

/airtime/fulfil

The request structure was similar to the data purchase request.

The application needed:

  • Request ID
  • Product unique code
  • Customer phone number
  • Amount

The request body was:

const postData = {

    requestId: requestId,

    uniqueCode: uniqueCode,

    details: {

        phoneNumber: phoneNumber,

        amount: amount

    }

};

The main difference is the endpoint:

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

The same HMAC SHA512 function could be reused:

const paymentHash = generatePaymentHash(postData);

The required headers were then created:

const headers = {

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

    'PaymentHash': paymentHash,

    'channel': 'pos',

    'Content-Type': 'application/json'

};

The complete airtime purchase function was:

const airtimeBillPayment = async (

    requestId,

    uniqueCode,

    phoneNumber,

    amount

) => {

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

    const postData = {

        requestId: requestId,

        uniqueCode: uniqueCode,

        details: {

            phoneNumber: phoneNumber,

            amount: amount

        }

    };

    const paymentHash = generatePaymentHash(postData);

    const headers = {

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

        'PaymentHash': paymentHash,

        'channel': 'pos',

        'Content-Type': 'application/json'

    };

    await createAppLog(

        JSON.stringify({

            controller: "Utility.js",

            action: "AirtimeDataBillPayment",

            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);

    }

};

Notice that the overall implementation is very similar to the data purchase function.

This is one of the advantages of creating reusable functions such as:

generatePaymentHash()

Instead of duplicating the hashing logic for every transaction type, the same function can be reused.

Logging the Requests

Another important part of the integration was application logging.

When working with an external API, logging can be very useful when troubleshooting failed requests.

For example:

await createAppLog(

    JSON.stringify({

        controller: "Utility.js",

        action: "AirtimeDataBillPayment",

        parameter: postData,

        header: {

            ...headers,

            PaymentHash: "[HIDDEN]"

        },

        url: apiUrl

    })

);

The log records information such as:

  • Controller or service.
  • Action being performed.
  • Request parameters.
  • API URL.
  • Request headers.

However, sensitive information should not be written directly to logs.

For example, instead of logging the actual PaymentHash:

PaymentHash: paymentHash

the implementation uses:

PaymentHash: "[HIDDEN]"

The same principle should be applied to API keys, private keys, passwords, tokens, and other sensitive credentials.

Logs should help with troubleshooting without becoming a source of security problems.

Handling Errors

When communicating with an external service, error handling is important.

The request may fail because of:

  • Invalid credentials.
  • Invalid request parameters.
  • Invalid product code.
  • Invalid PaymentHash.
  • Network problems.
  • Timeout.
  • Service unavailability.
  • Provider-side errors.
  • Invalid customer information.

The implementation therefore uses a try...catch block:

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);

}

One useful feature here is optional chaining:

error.response?.data?.status

This prevents the application from throwing another error if response does not exist.

For example, a network failure may not provide an HTTP response at all.

The application can therefore safely attempt to retrieve the error information when it is available.

Exporting the Functions

After implementing the three operations, I exported them from the module:

module.exports = {

    dataProductList,

    airtimeBillPayment,

    dataBillPayment

};

Other parts of the application can then import these functions when they need to interact with XpressPay.

For example:

const {

    dataProductList,

    dataBillPayment,

    airtimeBillPayment

} = require('./services/XpressPay');

This keeps the integration logic contained within one module while allowing other parts of the application to use it.

Testing with Postman

Before connecting the integration to the application's frontend, I tested the endpoints using Postman.

This was an important part of the development process.

I used Postman to verify:

  1. The API base URL.
  2. Authentication.
  3. Request headers.
  4. Request payload.
  5. Product codes.
  6. PaymentHash generation.
  7. Customer phone number.
  8. Transaction amount.
  9. API response.
  10. Error responses.

Testing with Postman first made the development process easier.

If a request failed in the Node.js application, I could compare it with the request that worked or failed in Postman.

This helped isolate whether the problem was coming from the API, the request structure, authentication, or my application code.

Lessons from the Integration

There were several useful lessons from this integration.

1. Study the API documentation first

The API documentation is effectively the contract between your application and the service provider.

Understanding the endpoints, request structures, headers, authentication requirements, and response formats before coding can save significant debugging time.

2. Test with Postman

Testing the API independently before implementing it in code makes it easier to identify problems with the API request.

3. Keep credentials out of the source code

API keys and private keys should be stored securely using environment variables or a proper secrets-management solution.

4. Understand the signing mechanism

If an API uses HMAC SHA512 or another signature mechanism, understand exactly what data is expected to be signed.

5. Reuse common logic

The data and airtime transactions use similar authentication and hashing logic.

Creating a reusable function such as:

generatePaymentHash()

reduces duplication and makes the code easier to maintain.

6. Protect sensitive information in logs

Logging is useful, but private keys, API credentials, tokens, and transaction signatures should not be exposed unnecessarily.

7. Expect external services to fail

When your application depends on an external API, failures are always possible.

Good error handling helps your application respond appropriately when the provider is unavailable or a request is rejected.

Conclusion

Integrating XpressPay VAS services was a practical example of how Node.js can be used to communicate with an external VAS provider.

The implementation started with retrieving the available data products. Once the available products could be retrieved, the next step was implementing data purchase, followed by airtime purchase.

The integration required more than simply sending HTTP requests. I had to understand the API documentation, configure API credentials, construct the correct JSON payloads, generate an HMAC SHA512 PaymentHash using the private key, send authenticated requests with Axios, process responses, and handle errors.

The final Node.js module provided three main operations:

module.exports = {

    dataProductList,

    airtimeBillPayment,

    dataBillPayment

};

Although the implementation is relatively small, it demonstrates several concepts that are common when integrating third-party APIs: asynchronous programming, REST API communication, environment configuration, cryptographic hashing, request signing, error handling, logging, and reusable service functions.

For me, the most important part of the exercise was not just getting the API to respond successfully, but understanding the complete communication flow between the application and the VAS provider.

That understanding makes it easier to integrate additional services in the future using the same approach.

 Tags

Node.js, JavaScript, VAS Integration, Airtime API, Data API, REST API, Axios, HMAC SHA512, API Integration, Backend Development, Web Development, JavaScript API, API Authentication, Fintech APIs, Software Development