Browse the documentation
Ad Controller (Advanced)
Applies to: Publishers · needs the matching permission on your account
The Ad Controller is the JavaScript that runs on your site and turns your ad zone snippets into real ads. Most publishers never need this page — copy the snippet from the Code tab of an ad zone and you're done. This page is for developers who want to understand what the controller does, configure it directly, host it themselves, or keep it working when visitors run an ad blocker.
What this is#
One controller belongs to one website. It carries its own variable name, chosen when the controller is created — the examples below use goCtrl, but yours may differ, and the snippets on your Code tab always show the correct one. Every ad zone on the page registers itself with that one controller, which batches the requests, picks the right plugin per zone type, and renders the ads.
You'll find your controllers under Publisher → Ads Controller.
Loading the controller#
Hosted (default)#
One tag loads the controller. Nothing else belongs in it:
<script src="https://your-ad-domain/ctrl/your-controller-id.js"></script>
Both parts are placeholders. your-ad-domain is your ad-serving domain and your-controller-id is the numeric id of the controller — the only part that varies between controllers. Don't type either by hand: the ready-made tag, with your real domain and id already filled in, is on the controller's row under Publisher → Ads Controller and at the top of every ad zone's Code tab. One URL per controller means a page with ten ad zones downloads it once and every zone reuses it from cache.
Put this in the <head> of your site's shared template, so it loads once on every page. Then each ad slot only needs its own add() call — no loader tag repeated down the page, and every page is ready to carry an ad zone the moment you add one. This matters even more if you've turned on Visitor analytics for the site: that feature counts a pageview on any page the controller loads on, ad zone or not, so a shared-template placement is what makes every page count instead of just the ones that happen to carry an ad.
<head>
...
<script src="https://your-ad-domain/ctrl/your-controller-id.js"></script>
</head>
Two rules matter:
- It must execute before any
add()call. It's a plain synchronous tag, so putting it in<head>guarantees that for every slot on the page. Don't addasyncordefer. - Repeating it is harmless. The snippet on each ad zone's Code tab includes the loader so it works standalone when pasted on its own; the controller ignores every load after the first. Once the tag is in your template you can safely drop it from the individual snippets and keep just the
add()call.
Ads come from separate add() calls, one per ad zone — see Example code per ad zone type below.
Self-hosted#
On the controller's row, Download gives you a ZIP to host on your own domain:
| File | What it is |
|---|---|
<yourctrl>.js |
The controller and the plugins your zones use |
<yourctrl>.php |
Optional proxy script for the anti-adblock connector |
adblocker_detected.html |
The modal shown when an ad blocker is detected |
ctrltest.html |
A ready-made test page with one example per ad zone |
Self-hosting makes the controller a first-party file on your domain and lets you use the proxy connector below. The trade-off is that you re-download after adding new ad zone types, since the bundle only carries the plugins your zones needed at download time.
The generated test page shows the configuration block:
<script src="./goCtrl.js"></script>
<script>
goCtrl.ctrlId = 1234; // example only — your download carries your own id
goCtrl.debugOn = false; // true to log what the controller is doing
goCtrl.bypassAb = true; // enable the anti-adblock connector
goCtrl.lazyLoading = true; // only request ads as they scroll into view
goCtrl.connectors = {
1:{'isproxy':0,'type':'url','url':'//your-ad-domain/ctrl'},
2:{'isproxy':1,'type':'path','url':'./goCtrl.php'}
};
goCtrl.init(); // start the controller
</script>
Connector 1 is the direct route. Connector 2 is only tried when the first is blocked — see below.
add() parameters#
Every ad zone is registered with one goCtrl.add({...}) call. The Code tab fills these in for you; this is what they mean.
| Parameter | Applies to | Meaning |
|---|---|---|
plugin |
all | Which renderer to use |
sid |
all | Your ad zone id |
display |
zones that render in place | The id of the HTML element to fill. It must be empty — the controller overwrites everything inside it |
subid |
all | Optional free-text label that shows up in your statistics |
keywords |
all | Optional keywords for contextual targeting, when your account has it enabled |
responsive |
banner |
1 sets the image to 100% width and lets the height follow |
advertise |
overlay formats | The small "Advertisement" label; set it to "" to hide it |
isflt |
some formats | Renders the format as an interstitial instead of in place |
clickTags |
pop, fpa, videoslider |
Array of selectors that may trigger the format. Empty means the whole page |
ignoreTags |
pop, fpa, videoslider |
Array of selectors that must never trigger it |
watchChrome |
pop |
true holds the pop back on Chrome until the visitor has 3+ pageviews and 3 minutes in the session. Defaults to false |
settings |
creator_widget |
Per-format options, e.g. {"max-items":10} |
fn / fn_params |
banner |
Loads an external iframe, with a fallback ad if it's blocked |
clickTags and ignoreTags take standard CSS selectors, matched with querySelectorAll through the controller's own Tools helper — the old docs described jQuery wildcards, which no longer applies. Ids must be unique on the page or only the first match is used.
The old documentation listed a
traffictypeparameter. It no longer exists in the current controller — device targeting is set on the ad zone itself, not in the page code.
Example code per ad zone type#
Each example assumes the loader tag is already on the page. 11708551 stands in for your ad zone id, and where an element id appears it must match between the <div> and the display parameter. These are exactly what the Code tab generates.
Banner#
<div id="id_11708551"></div>
<script>goCtrl.add({"display":"id_11708551","sid":11708551,"plugin":"banner"});</script>
Add "responsive":1 to let the creative fill the container's width.
Banner (floating)#
A banner zone set to Floater display needs no container — it positions itself.
<script>goCtrl.add({"sid":11708551,"plugin":"floater","subid":"","advertise":"Advertisement"});</script>
advertise is the label text drawn on the overlay. Many jurisdictions require ads to be identifiable as such — set it to "" only if you label the placement yourself.
Native#
The three tpl_ templates are yours to restyle — {title}, {image}, {description}, {displayurl} and {href_itemid} are substituted per item.
<div id="id_11708551"></div>
<script>
goCtrl.add({"plugin":"native","sid":11708551,"display":"id_11708551",
"tpl_body":'{body}',
"tpl_row":'<div>{items}</div>',
"tpl_item":'<div><span class="title">{title}</span><br><a class="{href_itemid}" href="#">{image}<br/><span class="displayurl">{displayurl}</span></a></div>'
});
</script>
Native bar#
<script>goCtrl.add({"plugin":"native_bar","sid":11708551});</script>
Native web push#
<script>goCtrl.add({"plugin":"native_webpush","sid":11708551});</script>
Interstitial native#
<script>goCtrl.add({"plugin":"native","isflt":1,"sid":11708551});</script>
Fixed IM banner#
<script>goCtrl.add({"plugin":"fxim_banner","sid":11708551,"default_icon":"","default_title":""});</script>
IM (in-message)#
<script>goCtrl.add({"plugin":"im","sid":11708551});</script>
Pop#
<script>goCtrl.add({"plugin":"pop","sid":11708551,"clickTags":[],"ignoreTags":["body"],"watchChrome":false});</script>
clickTags and ignoreTags decide which clicks open the pop, and they are the pair worth understanding:
clickTagsis an allow-list. Empty (the default) means any click on the page can trigger it. Set it to["#player",".article a"]and only clicks inside those elements do.ignoreTagsis a deny-list, applied on top.["body"]means a click landing on bare page background doesn't count — only clicks on actual content do. This is the default because a stray click on empty margin is the most common cause of a pop your visitors experience as random.
Both take standard CSS selectors — element names (video), classes (.no-pop), ids (#player), or anything else querySelectorAll accepts. Two details that matter in practice:
ignoreTagsalso covers descendants. Excluding#playerexcludes clicks on everything inside it, which is normally what you want.bodyis the exception — it matches only a direct click on the background, not the whole page.- Ids must be unique. With duplicates only the first is matched.
Start restrictive; a pop that fires on every click is the fastest way to lose a visitor.
Display type is not in the tag at all — it is the Display Type dropdown in the zone's Pop Ad Settings, read at serve time, so changing it never needs a re-copy. Four modes, and this is exactly what each one does in a browser:
- Popup — the ad opens in a new tab in front of your page.
- Tab Up — the same as Popup; the label just says what happens.
- Tab Under — your page (or the link the visitor clicked) is opened again in a new tab, and the tab the visitor was on is redirected to the ad, so the ad ends up behind. Not recommended: Google treats this pattern as deceptive and it can cost you search ranking.
- Popunder — the ad opens in a separate browser window that tries to go behind your page. Current Chrome, Firefox and Safari no longer let a page push a window behind itself, so there it usually stays in front as a popup window.
There is no mode that reliably opens an ad behind the page in a modern browser; if Popunder and Popup look the same on your machine, that is the browser, not the zone.
watchChrome is the third knob, and it works on time rather than on selectors. Set it to true and the pop stays closed on Google Chrome until the visitor has seen at least 3 pages and spent at least 3 minutes in the same browsing session. Until both are true the click passes through as an ordinary click — nothing opens, and no impression is counted.
This one also exists as a checkbox on the ad zone itself — Wait for engaged visitors on Chrome, in the zone's Pop Ad Settings — which is the better place to set it: it takes effect without anyone re-pasting the tag. The tag parameter and the zone setting are OR'd together, so a tag copied while the box was still unticked can't switch it back off. Two limits:
- Only desktop Chrome is held back. Edge, Opera, Samsung Internet and Chrome on iOS never are.
- It only applies to the Tab Under display type. The other pop display types open directly and ignore it.
The session is per browser tab, restarts whenever the visitor arrives from another site, and is dropped after two hours — so it counts people browsing on through your own pages, not drive-by traffic.
Full-page fallback zone — also in the zone's Pop Ad Settings. Visitors whose browser is on the zone's Blocked browsers list normally get no pop at all; pick one of your own Full Page ad zones here and those visitors get a full-page ad from it instead, so the visit still earns. A common setup is to block Chrome from pops and fall back to a full-page ad. Only your own full-page zones can be chosen (create one on the same website first); None switches the fallback off.
Full page ad#
<script>goCtrl.add({"plugin":"fpa","sid":11708551,"advertise":"Advertisement"});</script>
Floater / interstitial banner#
<script>goCtrl.add({"plugin":"floater","sid":11708551,"advertise":"Advertisement"});</script>
Video slider#
<script>goCtrl.add({"plugin":"videoslider","sid":11708551,"advertise":"Advertisement"});</script>
Add "isflt":1 for the interstitial variant, which takes over the viewport instead of sliding in at the edge. isflt behaves the same way on the native format: same zone, same plugin, different presentation — so you don't need a separate zone to try both.
In-page video#
<div id="id_11708551" style="max-width:426px;max-height:240px;display:none"></div>
<script>
goCtrl.add({"plugin":"inpage_video","sid":11708551,"display":"id_11708551","skip_btn":true,"skip_after_sec":5,"show_countdown":true,"skip_position":"top_right","auto_hide":true,"controls":false,"muted":true,"autoplay":true,"loop":true,"advertise":"Advertisement"});
</script>
The container starts hidden — the controller reveals it once an ad is ready, so no empty gap appears when there's nothing to show.
Reel#
<div id="id_11708551" style="max-height:640px;max-width:480px"></div>
<script>
goCtrl.add({"plugin":"reel","sid":11708551,"display":"id_11708551","skip_btn":false,"auto_hide":false,"controls":true,"muted":true,"autoplay":true,"loop":true});
</script>
Video carousel#
<div id="id_11708551" style="max-width:426px;max-height:240px;display:none"></div>
<script>goCtrl.add({"plugin":"video_carousel","sid":11708551,"display":"id_11708551"});</script>
In-video (inside your own player)#
<div id="video_wrap"><!-- your player goes in here --></div>
<script>
goCtrl.add({"plugin":"invideo","playerwrapper":"video_wrap","sid":11708551,"playertype":"video","advertise":"Advertisement","close_play":"Close & Play","skip_banner_when_video":true});
</script>
See Video player integration for the other player types.
Creator widget#
<div id="id_11708551" style="height:100%; max-height:350px;"></div>
<script>
goCtrl.add({"plugin":"creator_widget","sid":11708551,"display":"id_11708551",
"settings":{
"max-items":10
}
});
</script>
max-items caps how many entries the widget renders. The container has a height but no width — on a real page it inherits both from your layout, so place it inside the column where it should sit rather than sizing it absolutely.
Tabs#
<div id="id_11708551"></div>
<script>goCtrl.add({"plugin":"tabs","sid":11708551,"display":"id_11708551"});</script>
The container can be the content node of an existing tab in your own UI — the controller only fills it, it doesn't build the tab strip.
Dynamic banner (iframe)#
<div id="id_11708551"></div>
<script>goCtrl.add({"plugin":"dyn_banner","sid":11708551,"display":"id_11708551"});</script>
The creative is served inside an iframe sized to the container, so give the <div> the dimensions you want in CSS. Use this over a plain banner when the creative needs its own isolated document.
Skin#
<script>goCtrl.add({"plugin":"skin","bindto":"body","sid":11708551,"subid":""});</script>
bindto is the element the background skin attaches to. body covers the whole page, which is what you usually want. Point it at a wrapper element instead if your layout has a fixed-width container that should keep its own background.
Footer code#
<script>goCtrl.add({"plugin":"footer_code","sid":11708551});</script>
No container needed — this zone injects its markup at the end of the page. It's the right home for third-party verification or measurement snippets you want served through your zone rather than hard-coded into the template.
Global functions#
Beyond add(), the controller exposes a small API. All of it is optional — use it when you need to react to what the controller is doing, or drive it from your own code.
onReady(fn)#
Runs your function once the controller has initialised and knows about the visitor. This is where browserInfo becomes available:
<script>
goCtrl.onReady(function(){
if (goCtrl.browserInfo.isdesktop) {
// desktop-only behaviour
}
if (goCtrl.browserInfo.ismobile) {
// phone or tablet
}
});
</script>
browserInfo carries these device flags, each 1 or 0:
| Flag | True for |
|---|---|
isdesktop |
Desktop and laptop computers |
ismobile |
Phones and tablets |
istablet |
Tablets only |
isiptv |
Smart TVs and set-top boxes |
A phone is therefore ismobile && !istablet — ismobile deliberately covers both so existing page code keeps meaning what it always did.
Alongside those you get country (3-letter code), ulang / alang for the visitor's language, and ipaddress. Register as many onReady callbacks as you like; they all run.
Use this rather than your own DOMContentLoaded if the decision depends on visitor data — that data arrives with the controller's first response, which is later than DOM-ready.
pause() and resume()#
<script>
goCtrl.pause(); // stop processing new ad calls
goCtrl.resume(); // continue, and display anything queued while paused
</script>
You don't need this to batch calls — add() already buffers, so a run of consecutive add() calls goes out as one request without any help. Reach for pause() when you want the controller to stop acting on the page altogether: while a modal or video of your own is on screen, during a route change in a single-page app, or while you tear down a section that has ads in it. Paused, the controller neither requests nor renders, and its hidden-ad housekeeping stands down. resume() picks up where it left off and displays whatever queued in the meantime.
onAbDetected(fn)#
Runs when the controller determines an ad blocker is active. The self-host bundle uses it to show the bundled modal:
<script>
goCtrl.onAbDetected(function(){
if (!goCtrl.bypassAb) {
goCtrl.openModal({'uri':'./adblocker_detected.html','bgcolor':'#000000','reopen_onhide':true});
}
});
</script>
openModal({uri, bgcolor, reopen_onhide})#
Opens a full-viewport overlay containing a page of your choice in an iframe. Used by the self-host bundle for the adblocker notice, but it's a general-purpose modal.
<script>
goCtrl.openModal({
'uri':'./adblocker_detected.html', // required — the page to load
'bgcolor':'#000000', // backdrop colour, default #000000
'reopen_onhide':true // re-open if it gets hidden, default false
});
</script>
| Parameter | Default | Meaning |
|---|---|---|
uri |
— | Required. URL loaded into the overlay's iframe. Called without it, nothing opens and a debug line is logged |
bgcolor |
#000000 |
Colour of the full-screen backdrop behind the iframe |
reopen_onhide |
false |
Poll once a second, and if the overlay is no longer visible, remove and re-open it |
Behaviour worth knowing before you use it:
- It appears after ~1.5 seconds, not instantly — enough for the page to settle first.
- Sizing is automatic. On desktop the iframe is 90% × 90%, centred. On mobile it's 100% × 100%, top-left. You don't control this from the call.
- There is no built-in close button. Whatever you load is responsible for closing itself, so a modal with no exit in its own markup is a modal the visitor can't dismiss.
reopen_onhideis for the adblock case. Element-hiding filters can hide the overlay without the visitor doing anything; this notices and re-opens. On a general-purpose modal it means a visitor who closes it gets it back a second later, which is usually not what you want.
backLog(stype, json)#
Assign your own function to receive controller events — useful for wiring your own analytics:
<script>
goCtrl.backLog = function(stype, json){
switch (stype) {
case 'error': console.log(json.msg); break;
case 'init_finished': break;
case 'click': console.log('Click: ' + json.adzone); break;
case 'view': console.log('Impression: ' + json.adzone); break;
}
};
</script>
Smaller helpers#
| Call | Does |
|---|---|
goCtrl.getVersion() |
Controller version string |
goCtrl.GetMyId() |
The controller id this page is running |
goCtrl.reset() |
Clears queued and pending ad calls — a hard reset before re-registering zones |
goCtrl.debugOn = true |
Verbose console logging |
goCtrl.Tools — the built-in DOM helper#
The controller ships its own small DOM library at goCtrl.Tools. It exists because the controller needs jQuery-style helpers without making every publisher load jQuery, and it's already on the page — so if you're writing glue code around your ad slots, you can use it instead of adding a dependency.
It's a thin layer over querySelector / querySelectorAll. Every function taking a selector accepts a CSS selector string, a live element, or an array of either.
<script>
goCtrl.onReady(function(){
var t = goCtrl.Tools;
if (!t.isvisible('#leaderboard')) {
t.show('#fallback-promo');
}
t.on('click', '.gallery img', function(e){
// delegated — works for images added later too
});
});
</script>
Selecting and reading#
| Call | Does |
|---|---|
getnode(sel[, root]) |
First match, as an element |
getnodes(sel[, root]) |
All matches (NodeList) |
find(root, sel) |
Search within a container |
findall(x) |
Normalises a selector, element, or array of them into a list |
parents(sel) |
Ancestor chain of the first match |
attr(sel, name[, value]) |
Get or set an attribute |
isvisible(sel) |
Whether it actually occupies space on screen |
isInFold(sel) |
Whether it's inside the current viewport |
width(sel) / height(sel) / domrect(sel) |
Measurements |
dw() / dh() / ww() / wh() |
Document and window width/height |
Changing the page#
| Call | Does |
|---|---|
html(sel, markup) |
Replace contents |
append(sel, markup) / prepend(sel, markup) |
Insert at the end / start |
remove(sel) |
Delete the matches |
show(sel) / hide(sel) |
Toggle display |
css(sel, {prop: value}) |
Set styles |
Events and timing#
| Call | Does |
|---|---|
docready(fn) |
Run once the DOM is ready |
on(event, sel, fn) |
Delegated listener — bound to body and matched on the target, so it covers elements added later |
hover(sel, over, out) |
Mouse enter/leave pair |
onResized(fn) / onScrolled(fn) |
Debounced window handlers |
now() |
Timestamp |
Effects#
fadeIn(sel, ms[, cb]), fadeOut(sel, ms[, cb]), slideDown(sel, ms[, cb]), slideUp(sel, ms[, cb]), animateDown(sel, ms[, cb]) — each takes an optional callback that runs when the animation finishes.
Environment#
| Call | Does |
|---|---|
isTouchDevice() |
Touch-capable browser |
is_chrome() / is_bot() |
Browser and crawler checks |
hasWebP(cb) |
Async WebP support test |
appendUrlParms(url, {k: v}) |
Add query parameters to a URL safely |
mergeDefaults(target, source) |
Fill in missing keys, for your own option objects |
These names are stable in the built bundle — the minifier shortens the controller's internal state but leaves
Toolsand the public API intact. Anything not listed here is internal and may be renamed without warning, so don't reach for it.
Ad blockers#
Ad blockers work from lists of known ad-serving domains. When one blocks the controller's request, ads stop even though everything is configured correctly.
Enable Anti Adblock when creating or editing the controller, and give it a proxy — a first-party path on your own domain that forwards the request to us. Because the request now goes to your domain, it isn't on the block list. This only works self-hosted, and only if you set up one of these.
The path is yours to choose. your-chosen-path in the examples below is only a placeholder — it can be any path on your domain: /assets/loader, /w/x9, /api/content, anything. The one rule is that it must match in two places: the path in your web-server rule, and the url of connector 2 in the controller's config. Change one, change the other.
In fact, don't use /ctrl, /goctrl or anything else obviously ad-related. Block lists learn common paths, and a distinctive path is the single thing that most affects how long this keeps working. Same goes for the controller's variable name, which you can set when you create it.
A PHP script — the <yourctrl>.php in the ZIP. Drop it next to the controller; it forwards the request and passes the visitor's IP through.
nginx
location /your-chosen-path {
proxy_set_header X-Real-IP $remote_addr;
proxy_pass https://your-ad-domain/ctrl;
}
Apache (.htaccess)
RewriteEngine on
RewriteRule "^your-chosen-path" "https://your-ad-domain/ctrl" [P]
A CNAME from a subdomain of your own to our ad domain, or a CDN path rule (Cloudflare and similar) that forwards a path to us.
Setups made earlier that forward to /eactrl.go keep working; new ones should use /ctrl, which carries no .go for block lists to match.
Forwarding the visitor's real IP matters — without it every request looks like it comes from your server, and targeting and reporting both break.
Visitor analytics#
The same controller object doubles as an opt-in visitor-analytics beacon — enable it once from a website's settings (Visitor analytics toggle) and no second script tag is needed; it rides the exact requests this controller already makes.
For this to see every page — not just the ones with an ad zone on them — the loader tag needs to be on every page. See Put this in the shared template above: once it's there, analytics coverage and ad-zone coverage are the same thing, no separate install step.
Once enabled, pageviews are tracked automatically, including client-side route changes on single-page apps — the controller hooks history.pushState/popstate itself, so you don't need to call anything on navigation. To log anything beyond a pageview, call:
goCtrl.Event('signup', { plan: 'pro' });
The first argument is the event name; the second, optional, is a plain object of your own key/value data. Event('pageview') also works if you want to force one manually (e.g. right after enabling the feature, to confirm it's wired up), but it's redundant once the automatic tracking is on.
See the Visitor analytics doc topic for what gets tracked, where to view the numbers, and privacy notes.
Video player integration#
The invideo plugin plays a video ad inside your existing player. It needs to know which player you run:
playertype |
player should be |
|---|---|
video |
The id of your <video> element |
jwplayer |
The player's JavaScript object |
ktplayer |
The player's JavaScript object |
flowplayer |
The player's JavaScript object |
playerwrapper is the element around the player — for HTML5 video that's the <div> around the <video> tag, for the others it's the element the player was loaded into. The downloaded test page includes a complete working example for your zones.
Troubleshooting#
| Symptom | Likely cause |
|---|---|
goCtrl is not defined |
The add() call ran before the controller loaded — check the loader tag comes first, and that it has no async/defer. On a JavaScript-built site (Angular, React, Vue) the loader can genuinely still be in flight when the block runs: copy the ad code again from the zone's Code tab, which now wraps the call so it waits for the controller |
| Nothing renders, no errors | The display element doesn't exist or has a different id. Ids must be unique on the page |
| Works for you, not for some visitors | Ad blocker. Set up a proxy as above |
| Ads appear only after scrolling | lazyLoading is on — that's intended |
| Element flickers or collapses | The container has no reserved height. Size it in your CSS |
| New ad zone type doesn't render (self-hosted) | The bundle predates that zone. Download the ZIP again |
Set goCtrl.debugOn = true to have the controller log what it requests and renders — the fastest way to tell "no ad was returned" apart from "the ad was returned and something on the page hid it".
Related#
- Getting Your Ad Code — the simple path: copy the snippet and paste it
- Ad Zones — creating the zones the controller renders
- Ad Zone Types — what each zone type is and how it behaves
- Visitor Analytics — the pageview counter this controller feeds
- Websites — the site a controller belongs to
- How to serve ads from your own domain — step-by-step guide
- How to make banner ads responsive — step-by-step guide
- How to set up an in-video zone — step-by-step guide