Skip to main content

Overview

The first step in integrating mobile top-ups is to fetch the list of available plans from the API. Plans include all operators (Mobilis, Djezzy, Ooredoo, Pixx) with both dynamic and fixed amount options.
Plans are stable and rarely change. Cache them in your database to reduce API calls and improve performance.

API Reference

View complete API documentation for the GET /v3/mobile/plans endpoint

Understanding Plan Types

Dynamic Plans

Dynamic plans allow variable amounts within a min/max range:
{
  "code": "PREPAID_DJEZZY",
  "name": "Djezzy Prepaid",
  "operator": "Djezzy",
  "cost": 0.98,
  "isEnabled": true,
  "min_amount": 50,
  "max_amount": 5000
}
Use cases:
  • Prepaid top-ups (credit)
  • Postpaid bill payments (facture)
  • International calling credit

Fixed Plans

Fixed plans have predetermined amounts:
{
  "code": "PIXX_500",
  "name": "Pixx 500 DA",
  "operator": "Pixx",
  "cost": 0.97,
  "isEnabled": true,
  "amount": 500
}
Use cases:
  • Special promotional offers
  • Data packs
  • Service activations

GetMenu Plans

Special plans that retrieve available offers for a specific number:
{
  "code": "GETMENU_Mobilis",
  "name": "Mobilis GetMenu",
  "operator": "Mobilis",
  "cost": 0.98,
  "isEnabled": true,
  "min_amount": 50,
  "max_amount": 5000
}
Returns: Array of suggested_offers with all available plans for that specific number.

Fetching Plans from API

Basic Request

curl https://api.oneclickdz.com/v3/mobile/plans \
  -H "X-Access-Token: YOUR_API_KEY"

Response Structure

{
  "success": true,
  "data": {
    "dynamicPlans": [
      {
        "code": "PREPAID_MOBILIS",
        "name": "📱 PREPAID | عادي",
        "operator": "Mobilis",
        "cost": 0.96,
        "isEnabled": true,
        "min_amount": 40,
        "max_amount": 3999
      },
      {
        "code": "GROS_MOBILIS",
        "name": "📱 GROS MOBILIS | جملة",
        "operator": "GMobilis",
        "isEnabled": true,
        "min_amount": 5000,
        "max_amount": 300000
      }
    ],
    "fixedPlans": [
      {
        "code": "MIX500_MOBILIS",
        "name": "📱🌐 AUTO | MIX 500",
        "operator": "Mobilis",
        "isEnabled": true,
        "cost": 0.96,
        "amount": 500
      },
      {
        "code": "MIX1000_OOREDOO",
        "name": "📱🌐 AUTO | MIX 1000",
        "operator": "Ooredoo",
        "isEnabled": true,
        "cost": 0.99,
        "amount": 1000
      }
    ]
  },
  "meta": {
    "timestamp": "2025-10-29T00:35:59.220Z"
  }
}

Understanding the Cost Field

The cost field represents your wholesale price multiplier:
  • How It Works
  • Calculating Sell Price
  • Important Note
Example: cost = 0.98For a 1000 DZD top-up:
  • Face value: 1000 DZD (what user receives)
  • Your cost: 1000 × 0.98 = 980 DZD (what you pay)
  • Your margin: 1000 - 980 = 20 DZD (2%)

Caching Plans in Database

Since plans rarely change, cache them locally with your pricing:

Database Schema

CREATE TABLE mobile_plans (
    id SERIAL PRIMARY KEY,
    code VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    operator VARCHAR(50) NOT NULL,
    type VARCHAR(20) NOT NULL, -- 'dynamic' or 'fixed'
    
    -- Cost info
    wholesale_cost DECIMAL(5,4) NOT NULL,
    sell_price_multiplier DECIMAL(5,4) NOT NULL,
    
    -- Amount limits
    min_amount INTEGER,
    max_amount INTEGER,
    fixed_amount INTEGER,
    
    -- Status
    is_enabled BOOLEAN DEFAULT true,
    is_visible BOOLEAN DEFAULT true,
    
    -- Metadata
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_mobile_plans_operator ON mobile_plans(operator);
CREATE INDEX idx_mobile_plans_enabled ON mobile_plans(is_enabled);
CREATE INDEX idx_mobile_plans_code ON mobile_plans(code);

Sync Function

const db = require('./database');

async function syncMobilePlans() {
  try {
    console.log('🔄 Syncing mobile plans...');
    
    // Fetch plans from API
    const plans = await fetchMobilePlans();
    
    // Calculate your pricing
    const PROFIT_MARGIN = 0.05; // 5% profit
    
    for (const plan of plans) {
      const isFixed = !!plan.amount;
      
      const planData = {
        code: plan.code,
        name: plan.name,
        operator: plan.operator,
        type: isFixed ? 'fixed' : 'dynamic',
        wholesaleCost: plan.cost,
        sellPriceMultiplier: plan.cost * (1 + PROFIT_MARGIN),
        minAmount: plan.min_amount || null,
        maxAmount: plan.max_amount || null,
        fixedAmount: plan.amount || null,
        isEnabled: plan.isEnabled,
        lastSyncedAt: new Date()
      };
      
      // Upsert (insert or update)
      await db.mobilePlans.upsert({
        where: { code: plan.code },
        update: planData,
        create: planData
      });
    }
    
    console.log(`✅ Successfully synced ${plans.length} plans`);
    
    // Log statistics
    const stats = {
      total: plans.length,
      dynamic: plans.filter(p => !p.amount).length,
      fixed: plans.filter(p => p.amount).length,
      enabled: plans.filter(p => p.isEnabled).length
    };
    
    console.log('📊 Plan statistics:', stats);
    
    return stats;
  } catch (error) {
    console.error('❌ Failed to sync plans:', error);
    throw error;
  }
}

// Run sync on startup
syncMobilePlans();

// Schedule daily sync at midnight
const cron = require('node-cron');
cron.schedule('0 0 * * *', () => {
  console.log('⏰ Running scheduled plan sync...');
  syncMobilePlans();
});

Serving Plans to Users

When users request plans, serve from your database with your pricing:
async function getUserPlans(filters = {}) {
  const { operator, type, enabled = true } = filters;
  
  const query = { isEnabled: enabled, isVisible: true };
  
  if (operator) query.operator = operator;
  if (type) query.type = type;
  
  const plans = await db.mobilePlans.findMany({
    where: query,
    orderBy: [
      { operator: 'asc' },
      { fixedAmount: 'asc' },
      { name: 'asc' }
    ]
  });
  
  // Transform for user display (remove wholesale cost!)
  return plans.map(plan => ({
    code: plan.code,
    name: plan.name,
    operator: plan.operator,
    type: plan.type,
    minAmount: plan.minAmount,
    maxAmount: plan.maxAmount,
    amount: plan.fixedAmount,
    // Don't expose wholesale cost!
    // Only expose what user needs
  }));
}

// API endpoint
app.get('/api/mobile/plans', async (req, res) => {
  try {
    const { operator } = req.query;
    const plans = await getUserPlans({ operator });
    res.json({ success: true, data: { plans } });
  } catch (error) {
    res.status(500).json({ 
      success: false, 
      error: error.message 
    });
  }
});

Filtering Plans

By Operator

const mobilisPlans = await getUserPlans({ operator: 'Mobilis' });
const djezzyPlans = await getUserPlans({ operator: 'Djezzy' });
const ooredooPlans = await getUserPlans({ operator: 'Ooredoo' });

By Phone Number

function getOperatorFromPhone(phone) {
  const prefixMap = {
    '065': ['Mobilis'],
    '066': ['Mobilis', 'Ooredoo'],
    '067': ['Mobilis'],
    '077': ['Djezzy', 'Mobilis'],
    '078': ['Djezzy'],
    '055': ['Ooredoo', 'Djezzy', 'Mobilis'],
    '056': ['Ooredoo'],
    '068': ['Pixx']
  };
  
  const prefix = phone.substring(0, 3);
  return prefixMap[prefix] || [];
}

async function getPlansForPhone(phone) {
  const operators = getOperatorFromPhone(phone);
  
  if (operators.length === 0) {
    throw new Error('Invalid phone number prefix');
  }
  
  const plans = await db.mobilePlans.findMany({
    where: {
      operator: { in: operators },
      isEnabled: true,
      isVisible: true
    }
  });
  
  return plans;
}

Best Practices

Always cache plans in your database. Plans rarely change, and caching dramatically improves performance.
Never expose the cost field to end users. Only show your retail pricing.
Set up a daily cron job to sync plans. This ensures you have the latest offerings and availability.
Always verify isEnabled before displaying plans. Disabled plans may be temporarily unavailable due to operator issues.
Calculate your selling price by adding a reasonable profit margin (typically 3-10%) to the wholesale cost.
Create indexes on frequently queried fields (operator, code, isEnabled) for fast lookups.

Next Steps