Documentation

Ad-Supported Access

How a guest earns free WiFi data by watching ads, how a tenant prices and targets those campaigns, and how the platform proves a view actually happened. This goes deeper than the Documentation page's summary — read that first for the short version.

1

What it is, end to end

Ad-supported access is the default way most guests get online: no voucher, no card, no login. A guest hits Connect on the captive portal's Home screen (home.html) and is shown 1-3 data bundle choices, rendered from that captive's own configured ad-unlock tiers. Picking one starts the first ad immediately — no separate "selection screen" delay. Ads typically run 15-30 seconds each; the guest watches the number required by the tier they picked, and access unlocks the moment they've watched enough.

There are two separate server-side paths that can grant the bundle, and both read the exact same tenant-configured tiers so the result is identical no matter which one fires: completing the on-screen ad sequence in the Home modal, or an automatic top-up triggered as ad-watch progress events stream in from the player in real time (useful if a guest already has a partial session and tops up mid-browsing rather than sitting through a full fresh sequence). A guest gets full, uncapped speed for the entire time they're actively watching ads, including a guest who already has data and is watching more just to top up — ad playback is never throttled by whatever speed cap their current session already carries.

2

Setting the tiers: data, ad count, duration

Out of the box every captive runs the same three platform-default tiers:

DataAds requiredAccess duration
100MB51 hour
250MB101.5 hours
500MB152 hours

A tenant replaces these from ISN Portal → Ad revenue → Ad-unlock bundles: up to 3 tiers, each set independently — data amount (1-100,000MB), ads required (1-50), access duration (anywhere above 0 hours up to 168, i.e. one week), and an optional per-tier speed cap (down/up in kbps; left at 0 it falls back to that tenant's own default device speed, same convention used on voucher presets). Each tier can also carry its own Fair Usage Policy override, separate from the tenant-wide default. One tier is enough if you'd rather keep it simple — the other two slots just stay unused. A tenant that hasn't touched this screen at all always shows the three defaults above, never a blank state, and saving fewer tiers than before (e.g. dropping from 3 to 2) is handled as a full delete-and-replace, so a tenant can never end up with a stale leftover tier.

Whatever is configured is enforced the same way on both grant paths described in Panel 1 — a guest gets the same bundle for the same number of ads regardless of which one actually fired the grant.

3

What advertisers pay: the 5 ad formats and per-view pricing

Five ad formats exist platform-wide, each with fixed, non-configurable playback behavior — what you control per captive is the price an advertiser pays per view and the minimum total campaign spend:

PlanPlatform default priceBuilt-in behavior
StandardR0.20 / viewSkippable after 15 seconds by default (advertiser can raise the skip delay, never remove it client-side — the server decides whether a plan is skippable at all)
Skip DisabledR0.30 / viewUnskippable — forced server-side regardless of anything sent from the player
Homepage TakeoverR0.32 / viewStandard skip rules, counted as an "impression" event for budget purposes
AggressiveR0.35 / view2× the delivery weight of Standard — shown roughly twice as often in rotation
Aggressive + Skip Disabled
("Max Impact" in the ISN Portal's pricing table)
R0.45 / viewUnskippable AND 2× delivery weight — the combination of the two rows above
Customizable / CustomAdvertiser-set rateNo fixed price; subject to a R15,000 minimum campaign spend by default

Change any of these from ISN Portal → Ad revenue → Ad pricing. Leave a field blank and that captive uses the platform default shown above until you set your own — nothing breaks and nothing charges the wrong amount in the meantime. A price change updates everywhere it's shown or charged for that captive specifically (the Marketing Page at /business, the Captive Dashboard's own campaign builder, every revenue report) — another captive's pricing is never affected. Every price is bounded server-side to between R0.01 and R1000 — this isn't just a sane-default guardrail, it's a real security constraint: goal views for a campaign are calculated as budget ÷ price, so a near-zero price would let one tiny-budget campaign claim effectively unlimited rotation slots.

Delivery weight isn't just the plan multiplier above — it also self-paces against the campaign's own budget and elapsed time (an under-pacing campaign gets boosted in rotation, an over-pacing one gets throttled back), clamped so no campaign can ever be starved to zero or run away unbounded.

4

Targeting: locations, age range, schedule, frequency cap, takeover

Every one of these is set per-campaign in the Captive Dashboard's campaign builder (business-dashboard.html, under Campaigns), and every one fails closed — if the platform can't actually confirm a guest qualifies, the ad is withheld rather than shown by default.

ItemDetails
LocationsRestrict a campaign to specific routers instead of every one the tenant owns (e.g. a promo for one branch only). Leaving everything checked — the default — runs it everywhere, including any router added later. If the connecting router's identity isn't known at all, a location-restricted campaign is excluded for that guest rather than shown by accident; a stale or forged router ID from outside the tenant's own fleet is silently dropped server-side when the campaign is saved, so it can never scope a campaign onto a router it doesn't own.
Age TargetingType a numeric min and/or max age. Leave both blank for all ages. If a range is set, a guest whose age isn't known at all (no date of birth on file) is excluded exactly like a guest confirmed to be outside the range — "unknown" is never treated as "old enough." The guest's age is derived from an X-User-Dob header the client sends from the guest's own profile data, checked server-side on every ad request.
Schedule (dayparting)Day-of-week checkboxes plus an optional hour range in SAST, 24-hour format (e.g. a lunch special restricted to Mon-Fri, 11:00-14:00). Outside the window the ad simply isn't shown; it doesn't pause the campaign or affect its budget or end date. Leaving every day checked and the hour fields blank runs it all the time.
Frequency CapOff by default (a guest can see a campaign as often as it's otherwise eligible). Turn it on and set a max view count per day, week, or month per guest; once a guest hits it, that one campaign stops being offered to them until the period resets (SAST-aligned) — every other active campaign is unaffected. This caps the campaign's own reach, not a competitor's — a low cap can mean its budget or goal-views target takes longer to spend, or never fully spends, so it's best reserved for a genuine reason to avoid repetition (e.g. a one-time event promo) rather than switched on by habit.
PriorityLow, Normal (default), High or Top. When several campaigns can show to the same guest, priority decides which one is picked more often: Low about half as often as Normal, High about twice, Top about three times. It only changes how often an ad is chosen, never who is eligible, so age, location, schedule, budget and frequency-cap rules all still apply first. The sponsor name guests see is the campaign's Ad Display Name (or the advertiser you pick for it).
Brand Takeover DayMakes one campaign the ONLY ad shown (fleet-wide, or restricted to whichever Locations it also targets) for one full calendar day. Only one takeover can be booked per calendar day per captive; picking an already-booked date shows a conflict warning immediately. A takeover still has to pass every other eligibility check first (age, location, budget, end date) — it only wins over every other active campaign a guest would otherwise have seen, never over the checks that would have excluded that guest anyway.
5

Proof-of-play: how the system knows a guest really watched

Every ad-watch second is reported to the server by the player — not assumed client-side — through two tracking paths (/api/ad/progress for per-second ticks, and the batched /api/ad/report, up to 100 events per request, 5MB body limit) that both converge on the same ad_views table, which is treated as the single authoritative source of truth for how many views a campaign has actually delivered. The cached views_delivered counter on a campaign's own row can lag or drift; wherever the two disagree, the real ad_views count wins, and a drifted counter is corrected and the campaign force-paused on first sight rather than left to over-serve.

Because the same playback can legitimately get reported through more than one of these paths, a 10-second per-(ad, guest) dedup window prevents the same view from being billed twice. Skip behavior can't be tampered with client-side either: whether a given plan is skippable at all is decided server-side purely from the plan name (Skip Disabled and Aggressive + Skip Disabled are hardcoded unskippable, skipSec forced to 0, no matter what the player sends back).

Budget enforcement is layered: the view-serving endpoint (/api/ads/active) excludes a campaign the instant its real view count reaches its required views, and the reporting endpoints independently force-pause a campaign (is_active:false, paused_reason:'budget_exhausted') the moment it crosses that line, closing the brief race window that's possible at very high concurrency. The advertiser gets milestone emails as their campaign's budget is consumed (50/75/90/100%, or 50/75/100% depending on which reporting path reaches the threshold first) so a campaign running out never surprises them.

Proof-of-play reports. Every campaign card under the Captive Dashboard's My Campaigns has a Proof of play button — pick a date range and download a branded PDF or CSV. It shows how many times the ad played, how many different people saw it, how many watched to completion, skipped, or clicked, broken down by location (router name, area, route or router group), by day, and by hour in South African time — up to 400 days per report. It counts viewers but never identifies them; nothing in it is personal information. An advertiser can only pull reports for its own campaigns; the captive's own admin can pull one for any campaign on its captive.

6

Revenue: who sees what, and who gets paid

A campaign's spend is tracked as views delivered × the tenant's own configured price for that plan, capped at its budget — on the tenant's My revenue tab and the Captive Dashboard's own reporting, revenue bucketed by the actual timestamp each view happened, not by when the campaign was created. No payment gateway sits between the tenant and the advertiser: ISN is never a party to that transaction and never takes a cut of it. How the tenant actually collects — invoice, EFT, cash — is entirely between them and the advertiser.

An Admin or IT team member can click Add Business on the Captive Dashboard to create a real, independent login for a local advertiser (business name + login email is enough — no card needed to create the account). That business logs into the same Captive Dashboard from then on to build and fund its own campaigns, see its own results and proof-of-play reports, and — where assigned — its own survey question results, without ever seeing the tenant's other data, other advertisers, or account settings. The tenant sets price and minimum spend once; every business it onboards funds its own campaigns from there.

6b

The advertiser's own view of the Captive Dashboard

Add Business creates a normal advertiser (account type "Business"). From then on the advertiser signs in at the same address and form as your own team, and the platform shows it a restricted dashboard automatically:

ItemDetails
My Campaign PerformanceA card per campaign: its status, the share of budget used, views delivered against the goal, and notices such as a campaign that has ended or run out of budget. Campaigns with a survey attached show a View results button.
Create CampaignAd name shown to guests (filled in for them, editable), headline up to 160 characters, video up to 500 MB or an image gallery of up to 15 pictures with a 5 to 300 second slideshow, call-to-action button (website, call, WhatsApp with an optional ready message, email, directions or none, with an optional button label), skip timer (15 s, 30 s or none; plans that are unskippable lock it), plan, optional custom price per view, total budget and a duration of days, weeks, months or years. A live box shows the goal views before submit.
TargetingThe same locations, age, schedule, frequency cap, priority and Brand Takeover Day controls described in panel 4, on the same form.
My Ad Billing & SpendA live tracker of what each of the advertiser's campaigns has spent (views delivered times the price), so both sides know what is due. Nothing here takes a payment.
Proof of playPDF or CSV for any date range up to 400 days, as in panel 5. Only the advertiser's own campaigns.
SettingsTheir own business details, password and two-step sign-in. Nothing about your business.
API IntegrationUnder Settings an advertiser can generate read-only keys (they start with psk_live_, shown once, 60 requests a minute) and pull their own figures into a spreadsheet, Power BI, Zapier or their own software: GET /api/v1/analytics, /api/v1/campaigns, /api/v1/invoices and /api/v1/keys/me on https://your-captive.isnfreewifi.co.za, with the key in an X-Api-Key header. A key sees only its own advertiser's data and cannot change anything. Steps and copy-ready code for JavaScript, Python and Google Sheets are on the public For Advertisers page.

Where an advertiser is cut off: it cannot see or change another advertiser's campaigns, your guests, your routers, your pricing screens or your revenue; the server checks that every campaign and report belongs to the signed-in advertiser, not just the page. Sponsor and bank accounts (a read-only login for multi-venue campaigns) are switched off for now, so Add Business offers only the normal advertiser type. Businesses that ask how to advertise can be sent to the public For Advertisers page.

7

Related: cross-venue campaigns

A separate, adjacent system lets one sponsor run the same campaign across several independently-owned captives at once, rather than setting it up manually with each one. A tenant invites other tenants into its own network (tenant_cross_venue_members — a tenant can belong to more than one different owner's network at the same time, so this is a one-way invitation relationship, not a symmetric partnership). A sponsor — a "bank" business account, onboarded the same way as any other advertiser via Add Business but flagged as a sponsor and restricted to a read-only cross-venue report instead of the normal dashboard — posts one campaign, which is snapshotted out to every current member venue as its own row (cross_venue_campaign_venues) that walks independently through posted → proposed/declined → approved/rejected → active → withdrawn, each with its own price and budget so a mixed-price campaign across venues never conflates one venue's numbers with another's.

Budget mode is per-campaign: individual (default) means each venue's own budget caps it alone, unaffected by any other venue; pooled means one shared Rand budget is drawn down by views at ANY participating venue, and every venue pauses together the moment the shared pool is exhausted. This is the mechanism behind a single sponsor (e.g. a bank or a national brand) running one coordinated campaign across a network of otherwise-unrelated captives.

8

Edge cases and limits worth knowing

  • A tenant can configure at most 3 ad-unlock tiers — enforced by a hard database trigger, not just the dashboard form, so even a raw scripted request to exceed it is rejected outright rather than silently truncated.
  • A campaign that reaches its end date pauses automatically, and is permanently deleted 14 days later if an admin hasn't reactivated it.
  • Ad-pricing fields are bounded R0.01-R1000 per plan; minimum campaign spend must be 0 or greater. There is no upper bound on a tenant's own custom ad-unlock tier values beyond the field limits themselves (100,000MB, 50 ads, 168 hours).
  • If ads are the only access mode a captive offers (no vouchers, data plans, or surveys) and a guest has hit the Frequency Cap on every single active campaign, Connect automatically falls back to showing My Usage instead of a dead-end ad player with nothing left to show — the same fallback a brand-new captive with no ads at all already gets. It switches back the moment the guest's cap period resets, a new campaign is uploaded, or another access mode is turned on — no page reload required.
  • The public ad-serving endpoints (/api/ads/active, /api/ads/version) are rate-limited to 500 requests per 5 minutes per IP — enough for a captive portal polling every few seconds without false-positives, even across hundreds of millions of devices sharing the same deployment.
  • Clients poll /api/ads/version roughly every 60 seconds and only re-fetch the full active-ad list when its version counter has actually changed (new upload, pause, budget exhaustion, expiry) — this keeps ad-list updates reaching an in-session guest quickly without constant full re-fetches.