Hand Guests to Your Own Payment Portal (External Portal)
Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.
Summary The External Portal lets you use your own payment or authentication system for the hotspot. MKController’s captive collects the guest and hands off a one-time session token to your portal; after the guest pays, your server makes one API call with that token and a duration in minutes, and gets back a link that unlocks timed internet. Set it up in Integrations: turn it on, set your Redirect URL, generate an API key, and (optionally) add Wallet Garden domains your portal needs.
What Is the External Portal?
Section titled “What Is the External Portal?”Most WiFi Monetization flows use MKController’s own screens to collect data and take payment. The External Portal flips that: instead of paying on our captive, the guest is handed to your own external portal — your website, your checkout, your rules.
The mechanics are simple. MKController generates a session token for the guest, and with that token you decide who gets access and for how long. When you’re ready, you exchange the token for a redirect link that grants the guest a timed internet session on the MikroTik.
It is configured per voucher, and it is mutually exclusive with the built-in collect-data / payment flow — when the External Portal is on, the guest goes straight to your portal.
When to Use It
Section titled “When to Use It”- You already run a payment portal or membership system and want the Wi-Fi to use it.
- You need a currency, checkout, or business rule the built-in flow doesn’t cover.
- You’re an integrator building a custom guest experience on top of MKController.
If you just want guests to pay by card, Pix, or mobile money, use the built-in Payment Gateways and Pix instead — they need no code.
How to Set It Up
Section titled “How to Set It Up”Open WiFi Monetization, choose your voucher, open its configuration, and go to the Integrations section. Find the External Portal card.
Step 1: Turn it on and set the Redirect URL
Section titled “Step 1: Turn it on and set the Redirect URL”Enable External Portal and enter the Redirect URL of your own portal — the page guests land on to pay or sign in.
Step 2: Generate the API key
Section titled “Step 2: Generate the API key”Generate the company API key used to authenticate the session call. Copy it right away — the full key is shown only once (you can always Rotate it to get a new one). This is the same key surfaced under Voucher → Integrations → External Portal.
Step 3: Add Wallet Garden domains (optional)
Section titled “Step 3: Add Wallet Garden domains (optional)”Wallet Garden is the list of extra domains your portal needs to reach before the guest is authenticated — for example your payment gateway or a CDN/library host. Add each domain so the captive lets that traffic through pre-login. Click Add for each, then Save.
How the Handoff Works
Section titled “How the Handoff Works”-
A guest connects and hits the captive. Because the External Portal is on, MKController mints a one-time
sessionIdand redirects the guest to your portal:<REDIRECT_URL>?sessionId=<one-time-token> -
Your portal reads
sessionIdfrom the URL and runs your flow (payment, login, whatever you need). -
When the guest is cleared, your server calls the MKController session endpoint (below) with the
sessionIdand the number ofminutesto grant, authenticated with your API key. -
MKController opens a codeless session on the MikroTik (no personal data, just the device) and returns a
redirectUrl. Open it in the guest’s browser to complete the router login — internet is now live for the duration you set.
The sessionId is single-use and expires after 30 minutes, so a token can’t be replayed or shared.
The API Call
Section titled “The API Call”Your portal’s server makes this call (keep the API key on the server — never in the browser).
Request
curl -X POST 'https://app.mkcontroller.com/app/mkcontroller-server/rest/voucherExternalPortal/session' \ -H 'Authorization: Bearer mk_live_your_company_api_key' \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'sessionId=SESSION_ID_FROM_THE_URL' \ --data-urlencode 'minutes=60'Response (200)
{ "data": { "redirectUrl": "https://app.mkcontroller.com/captive/radius_connect.html?username=..." } }Parameters
| Field | Where it comes from | Notes |
|---|---|---|
Authorization | Voucher → Integrations → External Portal | Bearer <company API key> (mk_live_… / mk_test_…). |
sessionId | The ?sessionId= your portal received | Single-use, 30-minute TTL. |
minutes | You decide | Duration of the internet session to grant. |
redirectUrl | Returned by the call | Open it in the guest’s browser to complete the MikroTik login. |
Errors
401— invalid API key, or asessionIdthat belongs to another company.400—sessionIdis invalid, already consumed, or expired.
Security Notes
Section titled “Security Notes”- Keep the API key on your server. Anyone with it can open sessions for your account — rotate it if it leaks.
- The
sessionIdis single-use with a 30-minute TTL — it can’t be reused after you redeem it. - Sessions opened this way carry no personal data — only the device’s MAC — so nothing sensitive rides through the handoff.
Frequently Asked Questions
Section titled “Frequently Asked Questions”Do I need to write code? Yes — the External Portal is for teams running their own portal. If you don’t need that, use the built-in Payment Gateways or Pix instead.
Can I use my own currency or checkout? Yes. Payment happens entirely on your portal; MKController only unlocks access when you make the session call.
What’s the Wallet Garden for? It whitelists the extra domains your portal must reach before the guest logs in (payment gateway, CDN, libraries). Without it, that traffic is blocked pre-authentication.
How long is the session token valid? sessionId is single-use and expires after 30 minutes.
Where do the API key and Redirect URL live? In the voucher’s Integrations → External Portal card.
Explore next: Configure Payment Gateways · Receive Pix Payments · Link a Device · Get Paid — Payout & Financeiro
Building an integration? Talk to our team on WhatsApp or start your free MKController account.
Appendix: Minimal One-File Portal (for testing)
Section titled “Appendix: Minimal One-File Portal (for testing)”A tiny self-contained page that simulates the portal’s final step: it reads sessionId from the URL, sends the session call, and redirects the guest. Paste your company API key into API_KEY, host it anywhere (or on your MikroTik under the Wallet Garden), and open it with ?sessionId=<token>.
⚠️ This exposes the API key in the browser — it’s for testing only. In production, make the session call from your server.
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Portal — Grant access</title><style> *{box-sizing:border-box} body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center; background:#0f141b;color:#e6edf3; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif} .card{width:100%;max-width:380px;margin:18px;padding:22px; background:#182230;border:1px solid #2c3a4d;border-radius:14px} h1{font-size:19px;margin:0 0 16px} label{display:block;font-size:12px;color:#8b9bb0;margin:14px 0 5px} .val{font-family:"SFMono-Regular",Consolas,Menlo,monospace;font-size:13px; background:#0b1017;border:1px solid #2c3a4d;border-radius:8px;padding:9px 10px; word-break:break-all;color:#3ea6ff} input{width:100%;padding:10px;border-radius:8px;border:1px solid #2c3a4d; background:#1f2b3a;color:#e6edf3;font-size:15px} input:focus{outline:none;border-color:#3ea6ff} button{width:100%;margin-top:20px;padding:13px;border:none;border-radius:8px; background:#3ea6ff;color:#001;font-size:16px;font-weight:700;cursor:pointer} button:disabled{opacity:.5;cursor:not-allowed} .msg{margin-top:14px;font-size:13px;min-height:18px} .err{color:#f85149} .ok{color:#3fb950}</style></head><body> <div class="card"> <h1>Grant access</h1>
<label>Session ID</label> <div id="sid" class="val">—</div>
<label for="minutes">Session length (minutes)</label> <input id="minutes" type="number" inputmode="numeric" min="1" value="60">
<button id="go">Continue</button> <div id="msg" class="msg"></div> </div>
<script>(function(){ "use strict";
// ─────────── SET THESE ─────────── var API_KEY = "PASTE_YOUR_API_KEY_HERE"; // company key (mk_live_… / mk_test_…) var BASE_URL = "https://app.mkcontroller.com/app/mkcontroller-server/rest"; // ──────────────────────────────────
var byId = function(id){ return document.getElementById(id); };
// sessionId comes from the URL (?sessionId=...) — the captive redirects here with it function qs(name){ var m = new RegExp("[?&]"+name+"=([^&]*)").exec(window.location.search); return m ? decodeURIComponent(m[1].replace(/\+/g," ")) : ""; } var sessionId = qs("sessionId"); byId("sid").textContent = sessionId || "(no sessionId in the URL)";
function setMsg(text, cls){ var m = byId("msg"); m.textContent = text; m.className = "msg " + (cls||""); }
byId("go").addEventListener("click", function(){ var minutes = parseInt(byId("minutes").value, 10); if(!sessionId){ setMsg("No sessionId in the URL.", "err"); return; } if(!minutes || minutes < 1){ setMsg("Enter the time in minutes.", "err"); return; } if(API_KEY === "PASTE_YOUR_API_KEY_HERE" || !API_KEY){ setMsg("API key not set in the file.", "err"); return; }
var btn = byId("go"); btn.disabled = true; setMsg("Granting…", "");
var body = "sessionId=" + encodeURIComponent(sessionId) + "&minutes=" + encodeURIComponent(minutes); fetch(BASE_URL + "/voucherExternalPortal/session", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Bearer " + API_KEY }, body: body }) .then(function(res){ return res.text().then(function(t){ return {ok:res.ok, status:res.status, text:t}; }); }) .then(function(r){ var json = null; try{ json = JSON.parse(r.text); }catch(e){} var redirectUrl = json && json.data && json.data.redirectUrl; if(r.ok && redirectUrl){ setMsg("Access granted. Redirecting…", "ok"); window.location.href = redirectUrl; // completes the MikroTik login }else{ btn.disabled = false; setMsg("Failed (HTTP " + r.status + "). " + (r.text || ""), "err"); } }) .catch(function(e){ btn.disabled = false; setMsg("Network error: " + e, "err"); }); });})();</script></body></html>