How to Integrate Mobile Recharge API in Node.js (Step-by-Step)

Build a working mobile recharge feature in under an hour. Complete code walkthrough with error handling, webhooks, idempotency, and production deployment tips.

Mobile recharge is one of the most-used API integrations in Indian fintech. Whether you're building a wallet, a reward redemption platform, or a full recharge app, the core flow is the same: user enters a mobile number, your app processes the recharge, and you confirm success in real time.

In this tutorial, you'll build a complete, production-ready recharge integration in Node.js using the API Express Mobile Recharge API. We'll cover authentication, error handling, webhooks, testing, and deployment — everything you need to ship.

By the end, you'll have working code you can drop into any Express application.

💡
Complete code is under 100 lines. This tutorial is longer than the code because we explain the reasoning behind each decision. The actual implementation is tight and production-ready.

Prerequisites & Setup

Before starting, make sure you have:

  • Node.js 18 or later — we'll use native fetch (no external HTTP library needed)
  • npm or yarn — for dependency management
  • A free API Express account — sign up here if you don't have one
  • Your API key — available in your dashboard immediately after signup

Basic familiarity with Express and async/await is assumed. If you're new to Node.js, check out our introductory guide first.

Step 1: Project Setup

Create a new Node.js project and install dependencies:

Terminal
mkdir recharge-demo
cd recharge-demo
npm init -y
npm install express dotenv

That's it — only two dependencies:

  • express — for the HTTP server
  • dotenv — to load API keys from environment variables

We'll use the native fetch API (available in Node.js 18+) instead of adding an HTTP client library. Fewer dependencies means fewer things to break in production.

Step 2: Environment Variables

Create a .env file in your project root. Never hardcode API keys in source code:

.env
API_KEY=sk_test_your_sandbox_key_here
API_BASE_URL=https://sandbox.api.apiexpress.in
WEBHOOK_SECRET=whsec_your_webhook_secret_here
PORT=3000
⚠️
Add .env to your .gitignore. Committing API keys to Git — even private repos — is one of the most common causes of key leaks. Do it now before you forget.

Add these lines to your .gitignore:

.gitignore
node_modules/
.env
*.log

Step 3: Create the Recharge Client

Create a small client module to encapsulate the recharge API calls. This keeps your endpoint code clean and makes the integration easy to reuse.

recharge-client.js
// recharge-client.js
require('dotenv').config();

class RechargeClient {
  constructor() {
    this.apiKey = process.env.API_KEY;
    this.baseUrl = process.env.API_BASE_URL;
  }

  async recharge({ mobile, amount, operator = 'auto', refId }) {
    const response = await fetch(`${this.baseUrl}/v1/recharge`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        mobile,
        amount,
        operator,
        ref_id: refId
      })
    });

    const data = await response.json();

    if (!response.ok) {
      const error = new Error(data.message || 'Recharge failed');
      error.code = data.code;
      error.statusCode = response.status;
      error.requestId = data.request_id;
      throw error;
    }

    return data;
  }

  async getTransaction(transactionId) {
    const response = await fetch(
      `${this.baseUrl}/v1/recharge/${transactionId}`,
      {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`
        }
      }
    );

    const data = await response.json();

    if (!response.ok) {
      const error = new Error(data.message || 'Transaction lookup failed');
      error.code = data.code;
      error.statusCode = response.status;
      throw error;
    }

    return data;
  }
}

module.exports = RechargeClient;

Two things to note about this client:

  • It throws structured errors. Any non-2xx response becomes an Error object with code, statusCode, and requestId fields — so your endpoint can handle them precisely.
  • It uses ref_id for idempotency. Passing a unique reference per recharge means retries won't process the same transaction twice.

Step 4: Build the Recharge Endpoint

Create the main server file with a single POST endpoint that accepts a recharge request:

server.js
// server.js
require('dotenv').config();
const express = require('express');
const RechargeClient = require('./recharge-client');

const app = express();
const client = new RechargeClient();

app.use(express.json());

app.post('/api/recharge', async (req, res) => {
  const { mobile, amount } = req.body;

  // Basic validation
  if (!mobile || !/^[6-9]\d{9}$/.test(mobile)) {
    return res.status(400).json({
      error: 'Invalid mobile number'
    });
  }

  if (!amount || amount < 10 || amount > 5000) {
    return res.status(400).json({
      error: 'Amount must be between ₹10 and ₹5,000'
    });
  }

  try {
    // Generate idempotency key
    const refId = `recharge_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;

    const result = await client.recharge({
      mobile,
      amount,
      refId
    });

    return res.json({
      success: true,
      transactionId: result.transaction_id,
      operator: result.operator,
      amount: result.amount,
      status: result.status
    });

  } catch (error) {
    console.error('Recharge failed:', error.code, error.message);

    return res.status(error.statusCode || 500).json({
      success: false,
      error: error.message,
      code: error.code
    });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

That's the complete recharge endpoint. About 40 lines of code, and it's production-ready for the happy path.

Let's test it. Start the server:

Terminal
node server.js

Then make a test recharge request in another terminal:

Terminal
curl -X POST http://localhost:3000/api/recharge \
  -H "Content-Type: application/json" \
  -d '{"mobile": "9876543210", "amount": 199}'

You should get a JSON response with a successful transaction. If you're using sandbox keys, no charges apply.

✅
Try these test cases: Use mobile 9876543210 for success, 9000000001 for operator failure, or an invalid number to see validation in action.

Step 5: Handle Errors Gracefully

The simple endpoint above handles the happy path. Production code needs to handle every failure type gracefully. Let's expand the error handling:

server.js (updated endpoint)
app.post('/api/recharge', async (req, res) => {
  const { mobile, amount } = req.body;

  // Input validation
  if (!mobile || !/^[6-9]\d{9}$/.test(mobile)) {
    return res.status(400).json({
      success: false,
      error: 'Please enter a valid 10-digit Indian mobile number'
    });
  }

  if (!amount || amount < 10 || amount > 5000) {
    return res.status(400).json({
      success: false,
      error: 'Recharge amount must be between ₹10 and ₹5,000'
    });
  }

  try {
    const refId = `recharge_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;

    const result = await client.recharge({ mobile, amount, refId });

    return res.json({
      success: true,
      transactionId: result.transaction_id,
      operator: result.operator,
      amount: result.amount,
      status: result.status,
      message: result.message
    });

  } catch (error) {
    console.error('Recharge error:', {
      code: error.code,
      statusCode: error.statusCode,
      requestId: error.requestId,
      message: error.message
    });

    // Map API errors to user-friendly messages
    const errorMap = {
      'INVALID_MOBILE': {
        status: 400,
        message: 'This mobile number is not valid. Please check and try again.'
      },
      'INVALID_PARAMETER': {
        status: 400,
        message: 'Invalid input. Please check your request and try again.'
      },
      'RATE_LIMIT_EXCEEDED': {
        status: 429,
        message: 'Too many requests. Please wait a moment and try again.'
      },
      'QUOTA_EXCEEDED': {
        status: 503,
        message: 'Service temporarily unavailable. Please try again later.'
      },
      'INTERNAL_ERROR': {
        status: 500,
        message: 'Recharge failed due to a technical error. Please try again.'
      },
      'RECHARGE_FAILED': {
        status: 422,
        message: 'The operator declined this recharge. Please check the number and amount.'
      }
    };

    const mapped = errorMap[error.code] || {
      status: error.statusCode || 500,
      message: 'Recharge could not be completed. Please try again.'
    };

    return res.status(mapped.status).json({
      success: false,
      error: mapped.message,
      code: error.code,
      requestId: error.requestId
    });
  }
});

Key improvements:

  • User-friendly error messages — Users don't need to see INVALID_MOBILE. They need a message they can act on.
  • Correct HTTP status codes — 400 for user errors, 429 for rate limits, 5xx for server-side issues.
  • Structured logging — Every error logged with code, statusCode, and requestId for debugging.
  • Fallback handling — Unknown error codes still return a sensible message.

Step 6: Webhooks for Async Status

Some recharges take a few seconds to complete because they depend on the operator network. For those, you'll want webhook notifications rather than polling the API.

Add a webhook endpoint that verifies the signature and processes the event:

server.js (webhook endpoint)
const crypto = require('crypto');

// Important: use express.raw() for webhooks, not express.json()
app.post(
  '/webhooks/apiexpress',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-apix-signature'];
    const rawBody = req.body.toString();

    // Verify signature
    if (!verifyWebhookSignature(rawBody, signature)) {
      console.warn('Invalid webhook signature');
      return res.status(401).send('Invalid signature');
    }

    // Parse the verified payload
    const event = JSON.parse(rawBody);

    // Handle different event types
    switch (event.type) {
      case 'recharge.completed':
        console.log('Recharge completed:', event.data.transaction_id);
        // Update your DB, notify user, etc.
        break;

      case 'recharge.failed':
        console.log('Recharge failed:', event.data.transaction_id);
        // Refund wallet, notify user, etc.
        break;

      case 'recharge.pending':
        console.log('Recharge pending:', event.data.transaction_id);
        // Update UI to show pending state
        break;
    }

    // Always acknowledge quickly
    res.status(200).send('OK');
  }
);

function verifyWebhookSignature(payload, signatureHeader) {
  if (!signatureHeader) return false;

  const parts = signatureHeader.split(',');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];

  // Reject old events (replay protection)
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
  if (age > 300) return false; // 5 minute window

  const signedPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(signedPayload, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}
⚠️
Use express.raw() for webhook routes. JSON parsing can reorder fields and break signature verification. This is the #1 mistake developers make with webhooks.

Step 7: Test Everything

Before deploying, test the full flow:

Test 1: Successful recharge

curl -X POST http://localhost:3000/api/recharge \
  -H "Content-Type: application/json" \
  -d '{"mobile": "9876543210", "amount": 199}'

Expected: {"success": true, "transactionId": "TXN...", ...}

Test 2: Invalid mobile number

curl -X POST http://localhost:3000/api/recharge \
  -H "Content-Type: application/json" \
  -d '{"mobile": "123", "amount": 199}'

Expected: 400 with "Please enter a valid 10-digit Indian mobile number"

Test 3: Operator failure (sandbox test number)

curl -X POST http://localhost:3000/api/recharge \
  -H "Content-Type: application/json" \
  -d '{"mobile": "9000000001", "amount": 199}'

Expected: 422 with operator declined message

Test 4: Webhook signature

For webhooks, you'll want to test with a real endpoint. Use webhook.site or ngrok during development to see incoming payloads without deploying.

✅
Use the sandbox for all testing. Sandbox calls are free, deterministic, and won't process real recharges. Switch to production keys only after all tests pass.

Step 8: Deploy to Production

Before going live, run through this checklist:

  • ✅ All tests pass in sandbox
  • ✅ Production API key is in environment variables — not in code
  • ✅ Webhook secret is configured and signature verification is tested
  • ✅ Error logging is in place — with request IDs for support
  • ✅ Idempotency keys are used — every recharge has a unique ref_id
  • ✅ HTTPS is enforced — production webhooks must use HTTPS
  • ✅ Rate limit handling works — respecting Retry-After headers
  • ✅ Monitoring is set up — error rate, response time, quota usage

Switch to production

Update your production environment variables:

API_KEY=sk_live_your_production_key
API_BASE_URL=https://api.apiexpress.in
WEBHOOK_SECRET=whsec_your_prod_secret

Deploy. Test a single recharge in production. Then ramp up traffic.

💡
Use different keys per environment. Sandbox keys for development, live keys for production. If you deploy the wrong key, you'll get a 401 INVALID_API_KEY — our API rejects mismatched keys immediately.

Summary & Next Steps

You've built a complete, production-ready Mobile Recharge integration in Node.js. Let's recap what you've accomplished:

  • ✅ Set up a Node.js project with proper environment configuration
  • ✅ Built a reusable recharge client with structured error handling
  • ✅ Created an Express endpoint with validation and error mapping
  • ✅ Added webhook processing with signature verification
  • ✅ Tested every scenario in sandbox
  • ✅ Prepared for production deployment

The complete code is under 150 lines — proof that API integrations don't need to be complicated when the API provider does their job well.

Where to go next

If you get stuck or have questions, reach out to our team. We respond within 4 hours on business days.

And if this tutorial helped you ship faster, sign up for a free account and see what else you can build in a day.

AE
API Express Team
Engineering & Product

The API Express engineering and product team writes about API integration, developer workflows, and building for the Indian B2B market. We've processed over 10 million API calls for 500+ businesses.

Start building today

Ship Your First Recharge Integration in Under an Hour

Get your free API key, follow this tutorial, and have a working recharge feature live before end of day. Your first 1,000 API calls are on us.

No credit card required 1,000 free API calls Sandbox included Live in under a day