Documentation

Partner Voucher Reselling

How a retail partner — a spaza shop, a till, a chain of stores — sells your WiFi vouchers for real cash or on a prepaid balance, with its own billing, POS API, and fraud protection. This is a different feature from Revenue-Share Partners, which pays a router host a cut of ad/data revenue instead.

1

What this is, and how it's different from a revenue-share Partner

A Partner account on ISN Free WiFi is exactly one of two kinds, never both: a revenue-share partner, who hosts one or more of your routers and earns a cut of the ad/data revenue those routers generate (covered on its own dedicated page), or a voucher reseller — the subject of this page — who doesn't host any router at all, and instead sells your prepaid WiFi vouchers to real customers, the same way a retailer sells airtime.

A voucher reseller might be a spaza shop selling a R5 WiFi voucher alongside airtime, a till at a taxi rank, or a chain like a supermarket reselling vouchers at every branch. They never need a router of their own — the "Add Router" step is skipped entirely for a reseller, since router assignment has nothing to do with what they do.

Internally, both kinds share the exact same underlying partners account record — a single column (voucher_mode) decides which kind a given Partner is: set to cash or prepaid, the account is a voucher reseller; left empty, it's a revenue-share partner. An account is never both at once, and there's no "convert" button between the two — a person who should do both just gets two separate logins.

2

The balance model — any amount, one shared pool

A reseller doesn't pick from a fixed list of preset amounts — they type any Rand amount a real customer is paying for (R1, R5, R400, whatever), and the system mints a voucher for exactly that face value. There are no fixed denominations to stock or run out of.

Redeeming this voucher works exactly like redeeming a 1Voucher or OTT Voucher gateway code (see the Vouchers guide) — it does not unlock a bundle directly. It credits balance. A guest's balance is one shared pool regardless of where it came from: a gateway voucher, a code the tenant generated itself, or a partner-resold code all feed the exact same balance, spent the same way afterwards on whichever bundle the guest picks.

3

Settlement — cash mode vs. prepaid mode

You choose a settlement mode per reseller, changeable at any time without losing their history:

ModeHow it worksWhat protects you
CashThe reseller sells a voucher to a real customer for cash, keeps their agreed share %, and owes you the rest. A running "amount owed" tally tracks this.A per-reseller maximum-outstanding-owed ceiling — cross it and they're blocked from generating further vouchers until you mark the cash as received.
PrepaidThe reseller tops up their own balance first (through your tenant's connected payment gateway), then generating a voucher of face value X debits their balance by X × (1 − share%) — e.g. a R5 voucher at a 20% share debits them R4.Hard block on generation the moment their balance runs out. No credit is ever extended in this mode — that's what makes it safe.

Switching a reseller's mode later does not erase or reset any cash tally they already owe from before the switch — that stays outstanding until you settle it, independent of whichever mode is active now.

Amount limits (min/max per voucher) and a credit limit (cash mode only) are both set per reseller, not platform-wide — a guardrail against typos, never a cap on a legitimate big sale you want to allow for a specific reseller.

4

Adding a voucher reseller

From the "Voucher Resellers" tab (a tab fully separate from the revenue-share "Partners" tab, so the two never get mixed up), "+ Add Reseller" asks for: name, phone and/or email, settlement mode (cash or prepaid), share percentage, minimum/maximum voucher amount, and — for cash mode — a credit limit. No router or group field exists here at all.

The reseller is created in pending status and sent an invite by SMS and/or email (whichever channel your tenant has configured) with a one-time link to set their own password. The invite copy is written specifically for a reseller — about generating and selling WiFi vouchers — not the generic "see your earnings" wording a revenue-share partner gets, since a reseller never earns a revenue share at all.

Once they set their password, their status flips to active automatically and they can log in with their phone number or email as the username. A phone number typed in any common South African format (+27…, 0…, with or without spaces) is normalized consistently everywhere this platform uses it, so the same person is always recognized as the same account.

5

The reseller's own dashboard

A reseller's login shows only what's relevant to them — Sell Vouchers (their landing tab), My Billing, News, and Settings — never a revenue-share partner's Overview/My Routers/Earnings/My Rate tabs, and the reverse is also true. Which set of tabs renders is decided by the same voucher_mode field, resolved fresh from the server at login, on every page refresh, and even mid-session if a support agent impersonates the account — never trusted from a stale client-side cache.

On Sell Vouchers, the reseller sees their current balance (prepaid mode) or amount owed (cash mode), a simple "type an amount, get a code" generator with an optional transaction-reference field (for tying a voucher back to their own till receipt), a live table of every voucher they've issued with its status, and — in prepaid mode — a "Top up balance" button that pays through your tenant's own already-connected payment gateway.

On My Billing, they see the same sales/spending/profit numbers and the same statement/settlement history you see on your side — nothing hidden, nothing that can be retroactively changed behind their back (see the fraud-protection section below for why that matters).

6

POS / API access, for a till or a retail chain

A reseller can generate their own API key from their dashboard (or you can generate one for them) so a real till or POS system can issue vouchers automatically, without anyone typing codes by hand. The API calls the exact same underlying function the dashboard button does — identical limits, identical balance/owed-tally checks, no separate or looser logic for API-issued vouchers.

  • Idempotency required — every API call needs an externalReference. Calling it twice with the same reference never mints a second voucher or double-debits balance, so a till that retries after a network blip is always safe.
  • IP allowlisting (optional) and a configurable daily issuance cap per key — a key meant for one fixed-IP till can be locked to it.
  • Velocity alerting — an hourly burst cap (a quarter of the key's daily cap, minimum 5) trips an early email warning if a key is suddenly issuing vouchers unusually fast, on top of the slower daily-cap alert — useful if a key ever leaks, since that's usually burned through fast.
  • A full integration guide (Node.js/Python/cURL samples, endpoint reference, error table, and a "keeping this key safe" checklist) is built into the dashboard itself, reachable from the POS/API panel, and written out in full in section 6b below.
6b

POS / API integration guide

Let a partner's own till or POS system generate a voucher the instant a customer pays, with nobody opening the dashboard for each sale. This is the same guide the dashboard shows under POS / API access, written out here so whoever builds the till side can read it without a login.

This key is real money, not just a password. It can generate vouchers against this partner's own balance or credit. Never put it in a mobile app, a browser page's JavaScript or a public GitHub repo, because anything that ships to a device or is visible to a visitor can be extracted. Always call this API from a server the retailer controls (a POS backend, a small service on the till itself, or an intermediary you host), never directly from a till's own screen or app bundle.
Keeping the key safe on the retailer's side

Most leaked keys are not stolen by a clever attack, they are left somewhere ordinary. A retail till is often the weakest link in the chain. Pass this list to whoever sets up the till:

Store it as a secretAn environment variable or a secrets file with restricted permissions. Never hard-coded in source, never in a config file that is committed to git or backed up to a shared drive.
One key per tillDo not share one key across every till in the shop, let alone every branch. If one till is compromised, lost or replaced, you revoke exactly that key and every other till keeps working.
Lock it to an IP if you canIf the till (or the server calling the API for it) has a fixed public IP, set it when the key is created. A stolen key is then useless from anywhere else.
Set a realistic daily limitA small shop does not need 200 vouchers a day. Set it close to the real worst-case daily volume. It bounds the damage a leaked key can do before anyone notices.
Revoke when a device changes handsTill replaced or sold, or a staff member with access leaves: revoke that key in the dashboard and issue a fresh one. It takes one click and works instantly.
Check "Last used"Shown next to every key in the dashboard. Activity when the shop was closed, or far more often than real sales, means something is wrong. Revoke first, ask questions after.
1. Prerequisites

Generate a key in the dashboard under POS / API access (a reseller does it from their own Sell Vouchers tab, or you can generate it for them). It is shown once. Copy it straight into the till system's secret storage.

2. Base URL and authentication
https://your-captive.isnfreewifi.co.za

Replace your-captive.isnfreewifi.co.za with your own captive's address: its custom domain if it has one (for example wifi.yourbusiness.co.za), or its free name.isnfreewifi.co.za address. The guide inside your dashboard fills your captive's address in for you, so you can copy the samples from there as they are. The platform's main address, https://isnfreewifi.co.za, reaches the same API and also works, because the key decides which captive and which partner a request belongs to. Send the key on every request as the x-api-key header. It starts with isn_live_. The key itself decides which partner a request belongs to, so there is no partner id to send.

3. Endpoints
POST /api/partner-agent/vouchers/issueGenerates one voucher for an amount. Safe to retry: calling it twice with the same externalReference never makes a second voucher or debits balance twice. It returns the same one again.
GET /api/partner-agent/vouchers/statusLooks up a voucher by the externalReference it was issued with. Use it when a request timed out and you are not sure it went through, or a receipt printer jammed and you need to recover the code.

Request body for issue

amountRequired. The amount the customer is paying, for example 50. Any positive amount, within the minimum and maximum set for this partner.
externalReferenceRequired, up to 200 characters. Your own unique id for this sale (till transaction id, receipt number). This is the idempotency key.
transactionReferenceOptional, up to 200 characters. A human-readable note such as the receipt number, for recovering a code later. It can be the same value as externalReference.

Response to issue (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 or hand to the customer. They type it into the WiFi login page themselves. partnerCost is what was debited from the partner's balance (prepaid mode). balance is the partner's balance or owed amount right after this sale.

Response to status lookup

{
  "ok": true,
  "code": "4829173650",
  "amount": 50,
  "partnerCost": 40,
  "status": "unused",
  "createdAt": "2026-09-30T14:02:11.000Z",
  "redeemedAt": null
}
4. Code examples

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 = 'https://your-captive.isnfreewifi.co.za';

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 with 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 = "https://your-captive.isnfreewifi.co.za"

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 for any POS system that can run a command.

curl -X POST https://your-captive.isnfreewifi.co.za/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 "https://your-captive.isnfreewifi.co.za/api/partner-agent/vouchers/status?externalReference=TILL-4471-20260930" \
  -H "x-api-key: $PARTNER_API_KEY"
5. Idempotency and retries

externalReference is the idempotency key. Tills lose their network connection mid-sale. If a request times out and you are not sure it went through, 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 happens the first time). Always build it from something already unique in your own system (till id plus sale id, or the receipt number) rather than a random value per attempt. A fresh random reference on every retry would make a fresh voucher every time instead of recovering the original.

6. Errors and limits
400Missing or invalid amount or externalReference, an amount outside the partner's minimum and maximum, or not enough balance or over the credit limit.
401Missing, malformed or unknown API key.
403Key revoked, IP not on its allowlist, the partner's account is not set up for API issuance, the partnership has ended, or the tenant is suspended for billing.
404(status lookup) No voucher found for that externalReference.
409That externalReference was already used for a different amount. This is a bug in your own retry logic, not a network problem.
429Rate limit, daily cap or the hourly burst limit. Back off and retry after a short wait. Limits are configurable per key on request.
500 / 503Temporary. Safe to retry with backoff.

Every failure response looks like {"ok": false, "message": "..."}. Always check ok rather than assuming a 200-range status means success.

7. Voiding a voucher: what is recorded

A voucher can only be voided while it is still unused. Once redeemed it can never be voided. When a reseller voids one from their dashboard (or you void it as admin), all of this happens together and none of it can be edited afterwards:

  • The voucher's status becomes Voided, with the time it happened and who did it ("by you" on the reseller's screen, "by admin" if the admin did it).
  • The amount the reseller paid for it (prepaid) or owed for it (cash) is put back through the same locked ledger as every other balance change, with the reason voucher_voided (or voucher_voided_by_admin) and the voucher code as the reference. It shows in the reseller's balance history and in your view of that reseller.
  • The voided count and refunded value appear on the reseller's monthly statement and in the billing totals, so the books always add up.
  • A security-log entry is written for every void, from either side, naming the reseller, the voucher and the refund.
7

Billing — statements, settlements, and the admin-wide overview

Every reseller has their own numbered, persisted billing records, never just a live running number:

  • Statements (STM-YYYY-NNNNN) — a snapshot of one period's activity (opening/closing balance, vouchers issued/voided, top-ups, settlements). Closes automatically on the 1st of every month, or either side can pull one on demand for the period-to-date at any time.
  • Settlements (STL-YYYY-NNNNN, cash mode) — a receipt recorded the moment you mark cash as received from a reseller, reducing their owed tally.
  • Both are downloadable as a branded PDF, with a plain printable HTML fallback if PDF rendering isn't available on a given deploy — a billing document should never simply become unobtainable.

On your side, the Voucher Resellers tab's billing overview shows total outstanding owed across every cash-mode reseller, total prepaid float held, how many resellers are near their credit limit, this month's activity platform-wide, and a sortable per-reseller table — worst-standing-first, with search once you have more resellers than fit on screen.

8

Fraud protection — this assumes either side could be dishonest

This feature was built around a simple, explicit premise: a reseller could try to under-report what they sold and pocket the difference, and an admin could try to dispute or quietly alter what a reseller is owed. Both directions are closed structurally, not just by trust:

ItemDetails
Reseller can't under-reportIn prepaid mode, theft is impossible by construction, since balance is debited the instant a voucher is generated, before it's ever sold. In cash mode, the owed-tally ceiling is the equivalent of "no credit extended" — cross it and generation stops.
Admin can't retroactively shortchange a resellerEvery voucher stores an immutable snapshot of its own face value and share percentage at the moment it was created, never a live reference to a setting you could edit later. A reseller's own billing view is read-only to you — you can't edit or hide it after the fact.
Every balance change is lock-protectedAll of it flows through one single, serialized ledger function, so two requests racing at the exact same instant (e.g. two tills sharing one API key, or the same voucher code being redeemed from two devices at once) can never both succeed and double-spend the same money. This was specifically verified under genuine concurrent load, not just reasoned about.
Carding/card-testing protectionA prepaid top-up spends your own merchant account on a real payment-gateway transaction, so repeated rapid top-up attempts (a sign of someone testing stolen card numbers through your gateway) are capped and logged.
Top-up crash-safetyIf the server crashes in the narrow window between a payment actually succeeding and the balance being credited, a retry (webhook or manual) safely completes the credit exactly once, never zero times and never twice.
9

Billing suspension & ending a partnership

If your own tenant subscription falls into billing suspension, every new-revenue-generating action for resellers is blocked platform-wide — voucher generation, top-ups, admin balance grants, and API-key creation — regardless of how much balance a reseller is still sitting on. Reading history, settling a real cash receipt, and voiding a voucher all stay available, since none of those create new exposure.

Cutting off a reseller ends new activity but honors what's already out there — any voucher they already sold to a real customer keeps working until it's redeemed or expires, rather than being voided outright. The relationship is wound down, not erased; outstanding cash owed is a separate matter you pursue independently of whether those vouchers still work. Reactivating them later restores exactly the status they were genuinely in before (pending if they'd never set a password, active if they had) — it's never hardcoded back to active regardless of history.

10

CSV export & reconciliation

Both a reseller and you can export a CSV of every voucher — code, amount, status, how it was issued, and both the API's own idempotency reference and an optional human transaction-reference — for matching against a till's own end-of-day reports. Any reseller-supplied free-text field that ends up in that export is sanitized against spreadsheet-formula injection before it's written, since this file is often opened later by someone more trusted than the person who typed that text.