Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sistemashm24/pagos_hotspot_api/llms.txt

Use this file to discover all available pages before exploring further.

A MikroTik captive portal is typically a plain HTML page — hosted on the router itself or on an external web server — that MikroTik redirects unauthenticated clients to when they first join the network. The Pagos Hotspot API turns that static page into a full point-of-sale: fetch available plans, collect and tokenize a card, and hand back internet credentials in one round-trip. This guide covers every API call you need to implement and provides complete JavaScript examples.
The captive portal page runs entirely in the browser. Your server never sees raw card numbers — only a single-use token produced by Conekta.js or the Mercado Pago SDK on the client side.

Overview: Three API Calls

Every captive portal integration needs exactly three API interactions:

1. Public Config

GET /api/v1/config/publicFetches the payment gateway public keys. Call this once on page load.

2. Plan Catalog

GET /api/v1/catalogo_perfiles_ventaLists the available internet plans for this router. Render them as selectable cards.

3. Process Payment

POST /api/v1/payments/pagar-conekta or POST /api/v1/payments/pagar-mercado-pagoSubmits the card token and triggers the full purchase flow.

Getting the Client MAC and IP Address

MikroTik injects the connecting device’s MAC address and IP into the captive portal redirect URL using built-in variables. When it redirects to your portal page, the URL looks like:
https://portal.example.com/?mac=AA:BB:CC:DD:EE:FF&ip=192.168.88.100&ssid=WiFi-HotSpot
Parse these at page load with standard URL query parameter handling:
const params = new URLSearchParams(window.location.search);
const clientMac = params.get("mac") || "";
const clientIp  = params.get("ip")  || "";
const clientSsid = params.get("ssid") || "";

console.log("Client MAC:", clientMac);
console.log("Client IP:", clientIp);
These values are passed directly to the payment endpoint so the API can attempt an auto-connection after a successful purchase.

Step 1 — Fetch Public Configuration

Call this endpoint on page load to get the Conekta and Mercado Pago public keys. Do not hardcode keys in your portal HTML.
async function loadPublicConfig(apiKey) {
  const response = await fetch("https://api.tuempresa.com/api/v1/config/public", {
    headers: {
      "X-API-Key": apiKey,
      "Content-Type": "application/json"
    }
  });

  if (!response.ok) {
    throw new Error("No se pudo cargar la configuración");
  }

  const config = await response.json();
  // config.conekta_public_key  → initialize Conekta.js
  // config.mercadopago_public_key → initialize MP SDK
  return config;
}

Step 2 — Load the Plan Catalog

Fetch the list of available plans and render them for the customer to choose from.
async function loadCatalog(apiKey) {
  const response = await fetch("https://api.tuempresa.com/api/v1/catalogo_perfiles_venta", {
    headers: {
      "X-API-Key": apiKey
    }
  });

  if (!response.ok) {
    throw new Error("No se pudo cargar el catálogo");
  }

  return await response.json(); // Array of plan objects
}

Step 3 — Tokenize the Card

Before calling the payment endpoint, the raw card data must be tokenized client-side by Conekta.js. The API never receives raw card numbers.
<!-- Load Conekta.js -->
<script src="https://cdn.conekta.io/js/latest/conekta.js"></script>
function tokenizeCard(cardData) {
  return new Promise((resolve, reject) => {
    Conekta.Token.create(
      {
        card: {
          number:    cardData.number,
          name:      cardData.name,
          exp_year:  cardData.exp_year,    // e.g., "26"
          exp_month: cardData.exp_month,   // e.g., "12"
          cvc:       cardData.cvc
        }
      },
      (token) => resolve(token.id),
      (err)   => reject(new Error(err.message_to_purchaser || "Error al procesar tarjeta"))
    );
  });
}

Step 4 — Process the Payment

Submit the card token alongside the selected plan and the client’s MAC/IP. Pass auto_connect: true to trigger automatic internet access after a successful charge.
async function payWithConekta({ apiKey, productId, cardToken, customer, mac, ip }) {
  const response = await fetch("https://api.tuempresa.com/api/v1/payments/pagar-conekta", {
    method: "POST",
    headers: {
      "X-API-Key": apiKey,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      product_id:      productId,
      card_token:      cardToken,
      customer_name:   customer.name,
      customer_email:  customer.email,
      customer_phone:  customer.phone,
      user_type:       "usuario_contrasena",
      mac_address:     mac,
      ip_address:      ip,
      auto_connect:    true
    })
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.detail || "Error al procesar el pago");
  }

  return await response.json();
}

Step 5 — Handle the Payment Response

Once the API returns, check auto_conexion.estado to decide what to show the user.
function handlePaymentResult(result) {
  const { usuario_hotspot, auto_conexion, producto } = result;

  if (auto_conexion.estado === "conectado") {
    // Device is already online — show a success screen
    showSuccessScreen({
      message: "¡Ya estás conectado! Disfruta de " + producto.nombre,
      credentials: usuario_hotspot // Show as backup in case they need to reconnect
    });
  } else {
    // Not auto-connected — show credentials for manual login
    showCredentialsScreen({
      username: usuario_hotspot.usuario,
      password: usuario_hotspot.contrasena,
      message:  auto_conexion.mensaje
    });
  }

  // Always persist credentials for future auto-reconnect attempts
  localStorage.setItem("hs_username",   usuario_hotspot.usuario);
  localStorage.setItem("hs_password",   usuario_hotspot.contrasena);
  localStorage.setItem("hs_stored_mac", auto_conexion.mac || "");
}

Returning User Flow — Auto-Reconnect on Page Load

When the captive portal page loads, check localStorage for saved credentials. If found, attempt auto-reconnect before showing the purchase UI.
async function tryAutoReconnect(apiKey, currentMac, currentIp) {
  const username   = localStorage.getItem("hs_username");
  const password   = localStorage.getItem("hs_password");
  const storedMac  = localStorage.getItem("hs_stored_mac");

  if (!username || !password) return false; // No saved session

  try {
    const response = await fetch("https://api.tuempresa.com/api/v1/hotspot/auto-reconnect", {
      method: "POST",
      headers: {
        "X-API-Key": apiKey,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        username:    username,
        password:    password,
        stored_mac:  storedMac,
        current_mac: currentMac,
        current_ip:  currentIp
      })
    });

    const data = await response.json();

    if (data.auto_conexion === "conectado") {
      showSuccessScreen({ message: "Reconectado exitosamente. ¡Bienvenido de vuelta!" });
      return true;
    }

    if (data.estado === "expirado") {
      // Plan expired — clear credentials and show the purchase UI
      localStorage.removeItem("hs_username");
      localStorage.removeItem("hs_password");
      localStorage.removeItem("hs_stored_mac");
    }

    return false;
  } catch (err) {
    console.warn("Auto-reconnect failed:", err);
    return false;
  }
}
On page load:
(async () => {
  const params     = new URLSearchParams(window.location.search);
  const currentMac = params.get("mac") || "";
  const currentIp  = params.get("ip")  || "";
  const API_KEY    = "jwt_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // from your backend config

  const reconnected = await tryAutoReconnect(API_KEY, currentMac, currentIp);

  if (!reconnected) {
    // Load config and catalog for the purchase UI
    const config = await loadPublicConfig(API_KEY);
    Conekta.setPublicKey(config.conekta_public_key);

    const plans = await loadCatalog(API_KEY);
    renderPlans(plans);

    showPurchaseUI();
  }
})();

CORS Configuration

The API server must allow requests from your captive portal’s origin. Add the portal domain to the BACKEND_CORS_ORIGINS environment variable on the API server.
If your portal is served directly from the MikroTik router (e.g., http://192.168.88.1/), add that IP to the CORS origins list. For externally hosted portals, add the full domain including protocol and port.
BACKEND_CORS_ORIGINS=["https://portal.tuempresa.com","http://192.168.88.1"]

Test Card Numbers

Use the following card numbers in Conekta’s sandbox environment to test different scenarios without real charges:
Card NumberResult
4242 4242 4242 4242Approved (paid)
4000 0000 0000 0002Declined
CVV: 123Any of the above
Expiry: Any future dateAny of the above

Build docs developers (and LLMs) love