Welcome back! Login to access your analytics
Business Name
Read-only — every campaign you sponsor here, and how each participating venue is performing.
Loading…
Real-time campaign performance tracking
Scroll to zoom · Drag to pan · Click points for details
Scroll to zoom · Drag to pan · Click points for details
Click bars to view details
Click bars to view details
Top 5 campaigns by total views
Distribution of Call to Action button clicks across campaigns
| Business | Campaign Name | Type | Status | Total Views | Goal Views | Completed | Call to Action Clicks | Avg Watch Time | Completion % | Actions |
|---|
| Campaign | Plan | Status | Views Delivered | Budget Views | Budget Used | Days Left | Weight | Actions | Delete |
|---|---|---|---|---|---|---|---|---|---|
| Loading... | |||||||||
| Business Name | Status | Actions | |
|---|---|---|---|
| Loading… | |||
| Time | Action | Target | Reason | IP |
|---|---|---|---|---|
| No support activity yet. | ||||
| Title | Severity | Status | Start | End | Audience / Posted to | Actions |
|---|---|---|---|---|---|---|
| Loading... | ||||||
Loading your campaigns...
| Advertiser | Total Budgeted | Revenue Earned | Campaigns | Active | % Utilisation | Actions |
|---|---|---|---|---|---|---|
| Loading... | ||||||
Which routers are actually earning their keep — a router flagged Underselling is bringing in under half your per-router average this period; Overselling is more than 1.5× it. The trend arrow compares each router against its OWN prior period, not the fleet — a router can be dropping even if it's still above average. A router shown offline explains low numbers on its own — check it before assuming it's a sales problem.
| # | Router | Ad views | Vouchers redeemed | Data plans sold | Subscriptions sold | Ad revenue | Voucher revenue | Data plan revenue | Subscriber revenue | Total | Trend | Status |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Loading... | ||||||||||||
The same revenue as the table above, rolled up by router group instead of by individual router — useful if you run more than one site, branch, or department (a city's Parks routers vs its Libraries routers, a franchise's branches) and want one number per group instead of per router. Manage groups from the Routers tab. Reuses the period picker above.
| Department | Routers | Ad revenue | Voucher revenue | Data plan revenue | Subscriber revenue | Total |
|---|---|---|---|---|---|---|
| Loading... | ||||||
| Invoice # | Date | Due Date | Description | Amount (ZAR) | Status | Action |
|---|---|---|---|---|---|---|
| Loading invoices… | ||||||
Your session is about to expire.
120
You'll be signed out automatically — save anything you're working on now.
How many times this ad played, at which locations, on which days and at what times, plus how many different people saw it. Share it with your client as proof the campaign ran.
| Question | Type | Business | Price/Budget | Age | Answers | Review | Actions |
|---|---|---|---|---|---|---|---|
| Loading… | |||||||
A guest picks one of these when they choose "Unlock with a survey" on your captive page — the more questions required, the more data they get.
Optional — only needed if you want several questions (usually from the same business) to share one combined price and budget instead of being priced separately.
Every login already requires a code sent to your email. An authenticator app (Google Authenticator, Authy, etc.) is an optional alternative that uses your phone instead — turn it on below if you would rather use that.
Scan this QR code with your authenticator app, then enter the 6-digit code it shows.
Can not scan? Enter this key manually:
Save these backup codes somewhere safe, shown once. Each works only once if you lose access to your authenticator app.
Every device currently logged into this dashboard. Signed into one you don't own and forgot to log out? Sign it out from here.
| Device | Location | Last active | |
|---|---|---|---|
| Loading… | |||
Use API keys to connect third-party platforms to ISN Free WiFi. Keys give read-only access by default. Each key is shown once at generation — store it securely.
Add your tracking pixel IDs to measure conversions from your ISN Free WiFi ad campaigns. IDs are injected into ad landing pages only — no PII is forwarded.
Choose which events trigger email alerts to your account email.
Your logo and brand colour are used on captive portal ad creatives shown to WiFi users.
Link your business profiles so ISN Free WiFi can display your social handles on the captive portal and route customers to your pages.
Every device currently logged into this dashboard. Signed into one you don't own and forgot to log out? Sign it out from here.
| Device | Location | Last active | |
|---|---|---|---|
| Loading… | |||
Every device currently logged into this dashboard. Signed into one you don't own and forgot to log out? Sign it out from here.
| Device | Location | Last active | |
|---|---|---|---|
| Loading… | |||
Admin keys carry elevated rate limits (300 req/min) and full platform scope. Store in a secrets manager — never in source code.
Promote a user to admin or revoke admin privileges. Only the primary admin (env ADMIN_EMAIL) can use this action.
| Title | Status | Effective Date | New Amount | Received | Action |
|---|---|---|---|---|---|
| Loading… | |||||
Every device currently logged into this dashboard. Signed into one you don't own and forgot to log out? Sign it out from here.
| Device | Location | Last active | |
|---|---|---|---|
| Loading… | |||
Connect your ISP management system or billing platform via API keys. Supports read, webhooks, and write scopes.
Choose when ISN Free WiFi sends you email alerts.
This is a one-time-per-session step. Paste your server's ADMIN_TOKEN value (from your .env file), click Authenticate, then all the operations below will unlock. The token is kept only in this browser tab — never sent to disk or shared.
Don't know the token? Check the .env file in your server folder (ADMIN_TOKEN=…) or ask your server administrator.
One domain/host per line. Wildcards supported (e.g. *.example.com). Changes take effect immediately without server restart. Requires Admin Server Token.
Receive real-time POST notifications at your HTTPS endpoint when events occur on the network.
Customise the captive portal and splash page that end-users see on your network.
This will block all clients and business accounts from accessing login, home, help, news, status and business pages.
Enter your password to confirm signing out this device.
An API key is a unique credential that lets external services — such as Zapier, Power BI, or your own app — securely read your ISN Free WiFi campaign data. Each key is generated once and never displayed again, so copy it before closing this window.
This is the only time this key will be displayed. Once you close this window, it cannot be retrieved. Copy it now and store it in a secure password manager.
https://isn-free-wifi.isnfreewifi.co.za
X-Api-Key: psk_live_<your key here>
{"ok":true,...}:
curl -H "X-Api-Key: psk_live_..." \
"https://isn-free-wifi.isnfreewifi.co.za/api/v1/keys/me"
All endpoints are under https://isn-free-wifi.isnfreewifi.co.za/api/v1/ and require the X-Api-Key header. Your key only returns your own business data — cross-tenant access is blocked at the server.
/api/v1/keys/me
Scope: any
{ "ok": true, "key": { "id": "uuid", "prefix": "psk_live_a1b2c3",
"label": "Zapier", "scopes": ["read"],
"rate_limit_per_min": 60, "last_used_at": "2026-03-25T..." } }
/api/v1/analytics
Scope: read
period=1d|7d|30d|all (default: 7d){ "ok": true, "period": "7d",
"summary": { "total_campaigns": 3, "active_campaigns": 2,
"views_in_period": 12450, "clicks_in_period": 312,
"click_through_rate_pct": 2.51,
"lifetime_views_delivered": 48200,
"lifetime_required_views": 60000 } }
/api/v1/campaigns
Scope: read
active paused budget_exhausted expired{ "ok": true, "count": 2, "campaigns": [
{ "id": "uuid", "name": "Summer Sale",
"plan": "Premium", "media_type": "video",
"status": "active", "is_active": true,
"views_delivered": 9200, "required_views": 20000,
"budget_percent": 46, "budget_zar": 4000,
"price_per_view": 0.20, "days_left": 14,
"created_at": "2026-02-10T...", "end_date": "2026-04-01T..." }, ... ] }
/api/v1/invoices
Scope: read
{ "ok": true, "count": 3, "total_spent_zar": 1840.00,
"invoices": [
{ "campaign_id": "uuid", "campaign_name": "Summer Sale",
"plan": "Premium", "budget_zar": 4000, "views_delivered": 9200,
"price_per_view_zar": 0.20, "total_spent_zar": 1840.00,
"status": "active", "created_at": "2026-02-10T..." }, ... ] }
| 200 | Success — JSON body with "ok": true |
| 401 | Unauthorized — key missing, invalid, expired, or revoked |
| 403 | Forbidden — key valid but scope does not allow this endpoint |
| 429 | Rate limit exceeded — wait 60 s. Check X-RateLimit-Remaining header |
| 500 | Server error — retry once; if persists contact support |
https://isn-free-wifi.isnfreewifi.co.za/api/v1/analytics?period=7dX-Api-Key, value = your psk_live_… key.summary.views_in_period) to the next step in your Zap — send to Slack, Google Sheets, email, HubSpot, etc.https://isn-free-wifi.isnfreewifi.co.za/api/v1/campaignsX-Api-Key. Value: your full key.campaigns list and click Transform Data./api/v1/analytics and /api/v1/invoices as separate queries, then relate them by campaign_id.function syncAnalytics() {
var KEY = PropertiesService.getScriptProperties().getProperty("API_KEY")
|| "psk_live_YOUR_KEY_HERE";
var url = "https://isn-free-wifi.isnfreewifi.co.za/api/v1/analytics?period=7d";
var resp = UrlFetchApp.fetch(url, { headers: { "X-Api-Key": KEY }, muteHttpExceptions: true });
var data = JSON.parse(resp.getContentText());
if (!data.ok) { throw new Error("ISN Free WiFi API error: " + (data.message || resp.getResponseCode())); }
// getActiveSpreadsheet().getActiveSheet() works whether the script
// is run from the editor or via a time-driven trigger.
var ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) { throw new Error("No spreadsheet found — open this script from inside a Google Sheet (Extensions → Apps Script)."); }
var s = ss.getActiveSheet();
var sum = data.summary;
s.getRange("A1").setValue("Views (7d)");
s.getRange("B1").setValue(sum.views_in_period);
s.getRange("A2").setValue("Clicks (7d)");
s.getRange("B2").setValue(sum.clicks_in_period);
s.getRange("A3").setValue("CTR %");
s.getRange("B3").setValue(sum.click_through_rate_pct);
s.getRange("A4").setValue("Active Campaigns");
s.getRange("B4").setValue(sum.active_campaigns);
}
PropertiesService.getScriptProperties().getProperty('API_KEY').https://isn-free-wifi.isnfreewifi.co.za/api/v1/analyticshttps://isn-free-wifi.isnfreewifi.co.za/api/v1/analytics.X-Api-Key = your key.views_in_period to a custom contact/company property.https://isn-free-wifi.isnfreewifi.co.za/api/v1/campaignsX-Api-Key, Value = your key (store it in Make's Environment Variables).budget_percent exceeds 80.GET. URL: https://isn-free-wifi.isnfreewifi.co.za/api/v1/analytics?period=7dX-Api-Key = your key. Store the key in n8n's Credentials → Header Auth for reuse.summary fields — push to Airtable, Notion, send an email alert, etc.# Test connectivity
curl -H "X-Api-Key: psk_live_..." \
"https://isn-free-wifi.isnfreewifi.co.za/api/v1/keys/me"
# Analytics (last 7 days)
curl -H "X-Api-Key: psk_live_..." \
"https://isn-free-wifi.isnfreewifi.co.za/api/v1/analytics?period=7d"
# All campaigns
curl -H "X-Api-Key: psk_live_..." \
"https://isn-free-wifi.isnfreewifi.co.za/api/v1/campaigns"
# Invoice / spend history
curl -H "X-Api-Key: psk_live_..." \
"https://isn-free-wifi.isnfreewifi.co.za/api/v1/invoices"
import requests, os
BASE = "https://isn-free-wifi.isnfreewifi.co.za/api/v1"
KEY = os.environ["API_KEY"] # never hardcode in source
HDR = {"X-Api-Key": KEY}
def get_analytics(period="7d"):
r = requests.get(f"{BASE}/analytics", params={"period": period}, headers=HDR)
r.raise_for_status()
return r.json()["summary"]
def get_campaigns():
r = requests.get(f"{BASE}/campaigns", headers=HDR)
r.raise_for_status()
return r.json()["campaigns"]
def get_invoices():
r = requests.get(f"{BASE}/invoices", headers=HDR)
r.raise_for_status()
data = r.json()
print(f"Total spent: R{data['total_spent_zar']}")
return data["invoices"]
if __name__ == "__main__":
stats = get_analytics("30d")
print(f"Views: {stats['views_in_period']}, CTR: {stats['click_through_rate_pct']}%")
for c in get_campaigns():
print(f"{c['name']}: {c['status']} — {c['budget_percent']}% used")
const BASE = "https://isn-free-wifi.isnfreewifi.co.za/api/v1";
const KEY = process.env.API_KEY; // set in .env
async function fetchApi(path, params = {}) {
const url = new URL(BASE + path);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url, { headers: { "X-Api-Key": KEY } });
if (!res.ok) throw new Error(`ISN Free WiFi API ${res.status}: ${await res.text()}`);
return res.json();
}
// Usage
const analytics = await fetchApi("/analytics", { period: "7d" });
console.log("Views:", analytics.summary.views_in_period);
const { campaigns } = await fetchApi("/campaigns");
campaigns.filter(c => c.status === "active")
.forEach(c => console.log(`${c.name}: ${c.budget_percent}% used`));
const { total_spent_zar } = await fetchApi("/invoices");
console.log(`Total spend: R${total_spent_zar}`);
<?php
$key = getenv('API_KEY');
$base = 'https://isn-free-wifi.isnfreewifi.co.za/api/v1';
function apiGet(string $path, array $params = []): array {
global $key, $base;
$url = $base . $path . ($params ? '?' . http_build_query($params) : '');
$ctx = stream_context_create(['http' => [
'header' => "X-Api-Key: $key\r\nAccept: application/json\r\n"
]]);
$body = file_get_contents($url, false, $ctx);
return json_decode($body, true);
}
$analytics = apiGet('/analytics', ['period' => '7d']);
echo "Views: " . $analytics['summary']['views_in_period'] . PHP_EOL;
$campaigns = apiGet('/campaigns')['campaigns'];
foreach ($campaigns as $c) {
echo "{$c['name']}: {$c['status']}" . PHP_EOL;
}
?>
business_id is read from the database row — not from any query parameter you send. You cannot override it.business_id. Business A cannot see Business B's data regardless of the key used.| Account Type | Requests / min per key |
|---|---|
| Standard Business | 60 req/min |
| SaaS / ISP | 60 req/min |
| Admin | 300 req/min |
Retry-After: 60 header. Check the X-RateLimit-Remaining response header to monitor usage. Contact info@isnfreewifi.co.za to request higher limits for automated pipelines.
If you suspect your key has been compromised or see unexpected last_used_at activity, revoke it immediately from the API Integration card and generate a new one. Report suspected abuse to info@isnfreewifi.co.za.
When a WiFi user taps your ad and lands on the linked page, ISN Free WiFi silently fires your tracking pixel in the background. This registers a conversion event in your ad platform so you can see how many real people from your WiFi campaign took action — without capturing any personal information.
1234567890123456)G- (e.g. G-XXXXXXXXXX)twq('init', 'o1234') — the code after init is your Pixel IDPrivacy: Only your pixel ID is stored on our servers. ISN Free WiFi never forwards any personal data (names, emails, IP addresses) to advertising platforms. Pixel fires report only that a click event occurred, not who clicked.
| Minimum size | 200 × 200 px — square is strongly preferred |
| File format | PNG or WebP with transparent background (best results on any portal background colour) |
| Max file size | 500 KB — portals load on mobile data, keep it light |
| URL format | Must begin with https:// — self-signed or plain http:// links are blocked by browsers |
Your brand colour is applied to call-to-action buttons and highlighted text on captive portal ad creatives. Use the colour picker or type the hex code directly (e.g. #34f5a2).
Preview tip: After saving, trigger a test campaign in the Admin portal to see a live preview of your captive portal creative with your logo and brand colour applied. Branding changes take effect on the next campaign creative generated.
| Campaign | Plan & Rate | Status | Views Delivered | Budget Used | Est. Spend | Remaining | Time Left |
|---|---|---|---|---|---|---|---|
| Loading… | |||||||
| Campaign | Plan & Rate | Status | Lifetime Views | Budget Used | Total Spent | Budget | Period |
|---|---|---|---|---|---|---|---|
| Click Refresh to load your full expense history. | |||||||
| Title | Effective Date | New Amount | Last Viewed | Sent | Actions |
|---|---|---|---|---|---|
| Loading… | |||||
| Invoice # | Date | Due Date | Description | Amount (ZAR) | Status | Actions |
|---|---|---|---|---|---|---|
| Loading… | ||||||
POPIA-compliant · daily anonymised network snapshots
| Date | Users | Sessions | Data MB | Avg MB | Bundles | Exhausted | Birthdays | Ad Views | Cmplt% | Peak Hr |
|---|---|---|---|---|---|---|---|---|---|---|
| Loading… | ||||||||||
| Identifier | Router | SSID | IP | ↓ Mbps | ↑ Mbps | Used MB | Remaining | Session | Exp (min) |
|---|---|---|---|---|---|---|---|---|---|
| Loading… | |||||||||
| # | Name | Email / Phone | DOB | Bundle MB | Remaining | Status | Trusted | Registered |
|---|---|---|---|---|---|---|---|---|
| Loading… | ||||||||
| # | Business | Contact | Phone | Type | Status | Last Login | Created | |
|---|---|---|---|---|---|---|---|---|
| Loading… | ||||||||
| # | Name | Phone | Group | Split % | Status | Created |
|---|---|---|---|---|---|---|
| Loading… | ||||||
| # | Identifier | Bundle MB | Router | Granted At |
|---|---|---|---|---|
| Loading… | ||||
| Router ID | Location / Business | IP | Status | Users | Data MB | ↓ Mbps | ↑ Mbps | Peak ↓ |
|---|---|---|---|---|---|---|---|---|
| Loading… | ||||||||
| # | Domain | Category | Visits | Share |
|---|---|---|---|---|
| Loading… | ||||
| # | Domain | Category | Visits | Share |
|---|---|---|---|---|
| Loading… | ||||
| Title | Status | Effective Date | New Amount | Created | Action |
|---|---|---|---|---|---|
| Loading… | |||||
Monitor and manage all registered WiFi routers in real time.
Loading…
| Router ID | Name / Location | Status | Devices | ↓ Mbps | ↑ Mbps | Last Seen | Actions |
|---|---|---|---|---|---|---|---|
| Loading… | |||||||
Persistent — data stays even when users are offline
| # | User / Device | Router | Status | Download | Upload | Total |
|---|---|---|---|---|---|---|
| Loading leaderboard… | ||||||
| # | Name | Total Unlocks | Last Unlock | Access Slot | Actions |
|---|---|---|---|---|---|
| Loading… | |||||
ROUTER_ID in the router agent script exactly.| # | Date Unlocked | Bundle (MB) | Used | Remaining | Expires | Status |
|---|---|---|---|---|---|---|
| Select a user to view unlocks. | ||||||
Enter your ISN Free WiFi Business email address or phone number and we’ll send you a secure reset link.
If that email or phone number is registered, a password reset link has been sent. It expires in 15 minutes and can only be used on this device and browser.
This password reset link is invalid or has expired.
Hello — please enter your new password below.
This invite link is invalid or has already been used.
Welcome, — choose a secure password to activate your account.
Split a router's revenue with whoever hosts it — a driver, a fleet owner, a venue.
Which channel should partner invites and cutoff notices go out on?
Saved| Partner | Mode | Balance | Status |
|---|
| Name | Contact | Group / split | Routers | Revenue (30d) | Status |
|---|
| Router | Status | Gross | Partner cut |
|---|
| Code | Amount | Partner cost | Status | Issued via | Reference |
|---|
A real, numbered paper trail for this partner's account -- every settlement gets a receipt, every closed period gets a statement. Statements close automatically on the 1st of each month, or generate one for the period-to-date any time.
| Statement # | Period | Opening | Closing | Type |
|---|
| Reference | Amount | Method | Note | Date |
|---|
Many retail tills/POS setups aren't locked down as tightly as a dashboard login -- treat this key as real money, not just a password. View the integration guide & security checklist before handing it to whoever's building the till side.
Copy this key now -- it will not be shown again.
| API key | Created | Last used | Limits |
|---|
| Amount | Provider | Status | Created |
|---|
Let a partner's own till/POS system generate a voucher the instant a customer pays -- no one has to open this dashboard for every sale.
Most leaked API keys don't get stolen by a clever attack -- they get left somewhere ordinary. A retail till environment is often the weakest link in the whole chain, not this platform. Pass this list on to whoever sets up the till:
| Store it as a secret | An environment variable or a proper secrets/config file with restricted permissions -- never hard-coded in source, never in a config file that gets committed to git or backed up to a shared drive. |
|---|---|
| One key per till | Don't share one key across every till in the shop, let alone every branch. If a specific till is compromised, lost, or replaced, you revoke exactly that one key and every other till keeps working. |
| Lock it to an IP if you can | If the till (or the server calling this API on its behalf) has a fixed/static public IP, set that when the key is created -- a stolen key is then useless from anywhere else, no matter who has it. |
| Set a realistic daily limit | A small shop doesn't need a 200-voucher/day allowance. Set it close to real, worst-case daily volume -- it bounds the damage a leaked key can do before anyone notices. |
| Revoke immediately if a device changes hands | Till gets replaced, sold, or a staff member with access leaves -- revoke that key from this dashboard and issue a fresh one for the replacement. Takes one click, takes effect instantly. |
| Check "Last used" | Shown in the table below. If it shows activity at a time the shop was closed, or far more often than real sales, that's your signal something's wrong -- revoke first, ask questions after. |
Generate a key above (in the "POS / API access" section) -- it's shown once, copy it into the till system's own secret storage immediately.
Send the key on every request as the x-api-key header -- it starts with isn_live_. The key itself determines which partner a request belongs to.
POST /api/partner-agent/vouchers/issue | Generates one new voucher for a given Rand amount. Safe to retry: calling it twice with the same externalReference never generates a second voucher or debits balance twice -- it just returns the same one again. |
|---|---|
GET /api/partner-agent/vouchers/status | Looks up a voucher by the externalReference it was issued with -- for when a request timed out and you're not sure whether it actually went through, or a receipt printer jammed and you need to recover the code. |
Request body -- issue
amount | required -- the Rand amount your customer is paying, e.g. 50. Any positive amount, subject to whatever min/max this partner has configured. |
|---|---|
externalReference | required, up to 200 chars -- your own unique ID for this sale (till transaction ID, receipt number). This is the idempotency key. |
transactionReference | optional, up to 200 chars -- a human-facing note, e.g. the exact receipt number, for recovering a code later if it's needed. Can be the same value as externalReference, or something more readable. |
Response (201 = newly issued, 200 = replay of an already-issued reference)
{
"ok": true,
"code": "4829173650",
"amount": 50,
"partnerCost": 40,
"balance": 160,
"externalReference": "TILL-4471-20260930"
}
code is what you print/hand to the customer -- they type it into the WiFi login page themselves. partnerCost is what this actually debited from this partner's balance (only relevant in prepaid mode). balance is this partner's balance/owed amount right after this transaction.
Response -- status lookup
{
"ok": true,
"code": "4829173650",
"amount": 50,
"partnerCost": 40,
"status": "unused",
"createdAt": "2026-09-30T14:02:11.000Z",
"redeemedAt": null
}
Node.js 18+ (built-in fetch). Keep PARTNER_API_KEY in an environment variable, never hard-coded.
const PARTNER_API_KEY = process.env.PARTNER_API_KEY; // isn_live_...
const BASE_URL = '__BASE_URL__';
async function issueVoucher(amount, externalReference, transactionReference) {
const res = await fetch(`${BASE_URL}/api/partner-agent/vouchers/issue`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': PARTNER_API_KEY
},
body: JSON.stringify({ amount, externalReference, transactionReference })
});
const data = await res.json();
if (!data.ok) throw new Error(data.message);
return data; // { code, amount, partnerCost, balance, externalReference }
}
// Example: a till sale just completed
issueVoucher(50, `TILL-${tillId}-${saleId}`, receiptNumber)
.then((v) => printReceiptLine(`WiFi code: ${v.code}`))
.catch((err) => console.error('Voucher generation failed:', err.message));
Python 3, using requests. Keep the key in an environment variable via os.environ, never hard-coded.
import os
import requests
PARTNER_API_KEY = os.environ["PARTNER_API_KEY"] # isn_live_...
BASE_URL = "__BASE_URL__"
def issue_voucher(amount, external_reference, transaction_reference=None):
res = requests.post(
f"{BASE_URL}/api/partner-agent/vouchers/issue",
headers={"x-api-key": PARTNER_API_KEY},
json={
"amount": amount,
"externalReference": external_reference,
"transactionReference": transaction_reference,
},
timeout=15,
)
data = res.json()
if not data.get("ok"):
raise RuntimeError(data.get("message", "Voucher generation failed"))
return data # {code, amount, partnerCost, balance, externalReference}
# Example: a till sale just completed
voucher = issue_voucher(50, f"TILL-{till_id}-{sale_id}", receipt_number)
print_receipt_line(f"WiFi code: {voucher['code']}")
For testing, or wiring into any POS system that can shell out to a command.
curl -X POST __BASE_URL__/api/partner-agent/vouchers/issue \
-H "Content-Type: application/json" \
-H "x-api-key: $PARTNER_API_KEY" \
-d '{
"amount": 50,
"externalReference": "TILL-4471-20260930",
"transactionReference": "RCPT-88213"
}'
Checking on a request that may have timed out:
curl "__BASE_URL__/api/partner-agent/vouchers/status?externalReference=TILL-4471-20260930" \ -H "x-api-key: $PARTNER_API_KEY"
externalReference is the idempotency key. Tills lose network connections mid-sale -- if a request times out and you're not sure it went through, just call it again with the exact same externalReference: you get the same voucher back (HTTP 200), never a second one or a second debit (HTTP 201 only fires the first time). Always derive it from something already unique in your own system (till ID + sale ID, receipt number) rather than a random value per attempt, or retries defeat the point -- a fresh random reference on every retry would generate a fresh voucher every time instead of recovering the original.
| 400 | Missing/invalid amount or externalReference, amount outside this partner's configured min/max, or insufficient balance/over credit limit. |
|---|---|
| 401 | Missing, malformed, or unknown API key. |
| 403 | Key revoked, IP not on its allowlist, this partner's account isn't scoped for API issuance, the partnership has ended, or the tenant is suspended for billing. |
| 404 | (status lookup) no voucher found for that externalReference. |
| 409 | That externalReference was already used for a different amount -- a real bug in your own retry logic, not a network hiccup. |
| 429 | Rate limit, daily cap, or the hourly burst-velocity limit -- back off and retry after a short wait. All configurable per key on request. |
| 500 / 503 | Temporary -- safe to retry with backoff. |
Every response includes {"ok": false, "message": "..."} on failure -- always check ok rather than assuming a 200-range status means success.
A read-only link for whoever's actually with this router day to day — no login needed. It shows today's ad views and YOUR OWN earnings for this router only, nothing else on your account.
Your combined earnings across every router you host.
Type the amount your customer is paying for and hand them the printed/written code. Redeeming it credits their account balance -- exactly like a 1Voucher or OTT Voucher.
| Code | Amount | Status | Reference | Created |
|---|
Your own account statements and settlement receipts -- the same records the admin sees on their side. Statements close automatically on the 1st of each month, or pull one for the period-to-date any time.
| Statement # | Period | Opening | Closing | Type |
|---|
| Reference | Amount | Method | Date |
|---|
Let a till/POS system generate vouchers for you automatically, instead of typing each one in here. View the integration guide & security checklist before handing a key to whoever's building the till side -- treat it as real money, not a password.
Copy this key now -- it will not be shown again.
| API key | Created | Last used | Limits |
|---|
| Amount | Provider | Status | Created |
|---|
The routers currently assigned to you and their live status.
| Router | Status | Connected now | Earnings (30d) | Driver link |
|---|
How much you've earned recently, broken down per router.
| Router | Ad revenue | Voucher revenue | Data plan revenue | Subscriber revenue | Gross (30d) | My cut (30d) |
|---|
Your current revenue-share percentage and where it comes from.