Live banners¶
A live banner is a small widget for your lobby. It shows what is happening in a 19flip game right now, and it updates by itself.
You add it with two lines of HTML. When a player clicks it, you get an event and launch the game with your usual launch-game call.
Available banners:
game_id |
Banner | What it shows |
|---|---|---|
bingo-bongo |
"Lotto BINGO BONGO" marquee board | The next draw with a countdown (MM:SS, HH:MM:SS, or 13d 04:52 when a day or more away). Then ticket sales, then the live draw: pot, players, cards in play, jackpot, and the last ball. |
The banner takes the full width of the element you put it in, from 280 px up to the full page width. Its height follows its content (about 140 px). See Size and layout.
How it works¶
- You get your public
banner_keyand a ready embed snippet fromGET /api/operator/v1/banners. embed.jscreates an<iframe>served by 19flip inside your container.- The iframe asks 19flip for the banner state every 3 seconds (every 15 seconds while the browser tab is hidden). It then draws the banner and reports its height to your page, so the iframe always fits.
- When there is nothing to show, the iframe collapses to height 0. This happens when there is no draw and no scheduled draw, the game is unavailable, or the banner is off for your account.
- On click, the iframe sends an
openmessage.embed.jsturns it into youronOpencallback and a DOM event. Your page then launches the game.
The banner never sees your player and never touches money. It shows public round data only. The data is the same for every visitor of your lobby, whether they are logged in or not.
Step 1. Get your embed code¶
Authentication is the same as for launch-game: the Api-Key header and your IP allowlist (Authentication).
200 OK:
{
"success": true,
"data": [
{
"game_id": "bingo-bongo",
"name": "Bingo Bongo",
"kind": "live-round",
"banner_key": "pk_f339caa67ade409de57a3804",
"embed_script_url": "https://{PROVIDER_HOST}/banners/v1/embed.js",
"currencies": [
{
"currency": "USD",
"iframe_url": "https://{PROVIDER_HOST}/banners/v1/frame.html?key=pk_f339caa67ade409de57a3804&game=bingo-bongo¤cy=USD&lang=en",
"embed_html": "<div data-19flip-banner=\"bingo-bongo\" data-key=\"pk_f339caa67ade409de57a3804\" data-currency=\"USD\" data-lang=\"en\"></div>\n<script async src=\"https://{PROVIDER_HOST}/banners/v1/embed.js\"></script>",
"state_url": "https://{PROVIDER_HOST}/api/public/v1/banners/pk_f339caa67ade409de57a3804/bingo-bongo/state?currency=USD"
}
]
}
]
}
| Field | Description |
|---|---|
game_id |
Game the banner belongs to. Launch this game on click. |
kind |
Banner type. live-round is a banner that follows a shared round. |
banner_key |
Your public banner key. Putting it in page HTML is safe: it only selects whose state to show. It is not your api_key. Never put api_key in a page. |
embed_script_url |
The loader script. |
currencies[] |
One entry per currency you can use. A game like Bingo Bongo keeps a separate pot and jackpot per currency, so pick the currency your player will play in. |
currencies[].embed_html |
A ready snippet (see Step 2). |
currencies[].iframe_url |
A plain iframe URL, if you do not want the script (see Without the script). |
currencies[].state_url |
The public state endpoint (see Banner state). |
A banner is listed only when the game would launch for you. The game must be active, enabled for your account, and the currency must be allowed. If a game is not listed, its banner is off for your account.
Step 2. Put it on the page¶
<div data-19flip-banner="bingo-bongo"
data-key="pk_f339caa67ade409de57a3804"
data-currency="USD"
data-lang="en"></div>
<script async src="https://{PROVIDER_HOST}/banners/v1/embed.js"></script>
Include the script once per page, however many banners the page has. The script finds every [data-19flip-banner] element when it loads.
| Attribute | Required | Description |
|---|---|---|
data-19flip-banner |
yes | game_id, e.g. bingo-bongo. |
data-key |
yes | Your banner_key. |
data-currency |
yes | Currency of the round to show, e.g. USD. |
data-lang |
no | en (default), ru, ua (uk is also accepted). |
data-click-url |
no | Where to go on click if nothing handles the click (see Step 3). |
data-hide-scheduled |
no | true shows only a running round, with no "next draw" countdown. |
Do not hide the container with display: none
An iframe inside a display: none element has no layout, so the banner cannot measure itself and stays at height 0.
embed.js already collapses the iframe when there is nothing to show. For spacing around an empty banner, use the data-19flip-visible attribute, e.g. .slot:not([data-19flip-visible]) { margin: 0 }, or the 19flip:banner-visibility event.
Size and layout¶
You control the size and position with CSS on your container, the element with data-19flip-banner or the one you pass to mount(). The iframe is always width: 100% of that element, and embed.js sets its height automatically. Never set the iframe height yourself.
| Layout | CSS on the container |
|---|---|
| Side column (the original design) | width: 360px;, or any width from 280 to 400 px |
| Wider, centered | max-width: 720px; margin: 0 auto; |
| Full width of the page or section | nothing, or width: 100%; |
| Several banners in a row | a flex or grid parent, e.g. display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; |
<style>
.bingo-banner { max-width: 720px; margin: 0 auto 24px; }
/* no gap while there is no draw */
.bingo-banner:not([data-19flip-visible]) { margin: 0; }
</style>
<div class="bingo-banner" data-19flip-banner="bingo-bongo"
data-key="pk_…" data-currency="USD" data-lang="en"></div>
- 280–400 px is the reference design, pixel for pixel. The drum with the last ball appears from 360 px.
- Wider than 400 px, the same board stretches. The wordmark stays on the left and the jackpot and button on the right. The light garland adds bulbs to keep the same spacing, and the ticker repeats its text so it runs without gaps. Height does not change.
- Narrower than 280 px is not supported. Even at 280 px, when a jackpot is shown, the wordmark touches the jackpot amount; use 320 px or more if you can.
- Colours, fonts, texts and the corner radius are fixed. The banner is the same on every casino. You choose the language with
lang. You can round or clip it further withborder-radiusandoverflow: hiddenon your container. data-19flip-visibleis set on the container only while the banner is shown. Use it for margins, headings or a frame around the banner.
Step 3. Handle the click¶
A click on the banner does not open the game by itself. You own the player session and the wallet, so the click is handed to your page. Pick one of these:
A. DOM event (works with the HTML snippet):
document.addEventListener('19flip:banner-open', (event) => {
const { game_id, currency, status } = event.detail
event.preventDefault() // stop the data-click-url fallback
openGame(game_id, currency) // your code: call launch-game, open launch_url
})
B. data-click-url: if no onOpen callback is set and no handler calls preventDefault(), the top page goes to this URL, e.g. your Bingo Bongo game page.
C. JS API (see below): pass onOpen.
event.detail / onOpen argument:
| Field | Type | Description |
|---|---|---|
game_id |
string | Game to launch. |
currency |
string | Currency of the banner. |
status |
string | Banner status at the moment of the click: scheduled, selling or drawing. You can use it, for example, to open the lobby of the game before sales start. |
JS API¶
embed.js sets window.NineteenFlip.banners. The script loads with async, so wait for it first. Either define window.NineteenFlipBannersReady = (banners) => { … } before the script tag, or listen for the 19flip:banners-ready event on document.
const banner = window.NineteenFlip.banners.mount(document.getElementById('bingo-slot'), {
key: 'pk_f339caa67ade409de57a3804',
game: 'bingo-bongo',
currency: 'USD',
lang: 'en',
onOpen: ({ game_id, currency }) => openGame(game_id, currency),
onStateChange: (status) => console.log('banner status', status),
onVisibilityChange: (visible) => slot.classList.toggle('has-banner', visible),
})
banner.update({ lang: 'ru' }) // switches language without reload
banner.destroy() // removes the iframe
| Function | Description |
|---|---|
mount(element, options) |
Creates the banner inside element and returns a handle. Mounting again into the same element replaces the old banner. |
scan(root = document) |
Mounts every not yet mounted [data-19flip-banner] inside root. Call it after you add banner markup dynamically (e.g. in a SPA). |
handle.update(options) |
Changes options. lang is applied live. key, game, currency and hideScheduled reload the iframe. |
handle.destroy() |
Removes the iframe and the listeners' target. |
mount options:
| Option | Type | Required | Description |
|---|---|---|---|
key |
string | yes | banner_key. |
currency |
string | yes | Currency code. |
game |
string | no | game_id, default bingo-bongo. |
lang |
string | no | en (default), ru, ua. |
hideScheduled |
boolean | no | Hide the "next draw" announcement. |
clickUrl |
string | no | Fallback navigation on click. |
onOpen |
function | no | Click handler, gets the object described in Step 3. |
onStateChange |
function | no | Called with the status every time it changes (unavailable first, until the first state arrives). |
onVisibilityChange |
function | no | Called with true/false when the banner appears or collapses. |
DOM events, all dispatched on the banner element and bubbling:
| Event | detail |
|---|---|
19flip:banner-open |
{ game_id, currency, status }; cancelable |
19flip:banner-state |
{ status } |
19flip:banner-visibility |
{ visible } |
Without the script¶
You can put iframe_url in your own <iframe>. The URL takes these parameters:
| Parameter | Description |
|---|---|
key, game, currency |
As above. Required. |
lang |
en, ru, ua. |
hide_scheduled |
1 to hide the announcement. |
id |
Any string. It is echoed in every message, so you can tell several banners apart. |
origin |
Your page origin, e.g. https://casino.example. Messages are then sent only to that origin; without it they are sent to *. |
You then handle sizing and clicks yourself through postMessage. Every message from the iframe looks like this:
type |
payload |
Meaning |
|---|---|---|
ready |
{ game_id } |
The iframe has loaded. |
resize |
{ height } |
Set the iframe height to this many pixels. It is 0 when there is nothing to show. |
visibility |
{ visible } |
The banner appeared or collapsed. |
state |
{ status } |
The status changed. |
open |
{ game_id, currency, status } |
The player clicked the banner. |
Check event.origin against the 19flip host and event.data.source === "19flip-banner" before trusting a message.
Your page can send { "source": "19flip-banner", "v": 1, "id": "<same id>", "type": "set-lang", "payload": { "lang": "ru" } } to the iframe to change the language.
Protocol version v: 1 and the /banners/v1/ path only change compatibly: new message types or fields may be added, and existing ones are never changed. A breaking change will get a new path, /banners/v2/, and v1 will stay available next to it.
Banner state¶
Build your own banner UI, or render the state on your server, with the same data the widget uses.
- With your API key:
GET /api/operator/v1/banners/{game_id}/state?currency=USDwith theApi-Keyheader. - Public (from a browser, no key):
GET /api/public/v1/banners/{banner_key}/{game_id}/state?currency=USD. It has CORS for any origin andCache-Control: public, max-age=2.
Both return:
{
"success": true,
"data": {
"game_id": "bingo-bongo",
"status": "drawing",
"server_time": "2026-09-26T14:49:25.133163Z",
"currency": "USD",
"round": {
"number": 15,
"is_demo": false,
"sale_closes_at": "2026-09-26T14:44:30.525920Z",
"players_count": 6,
"cards_count": 13,
"bank": "13.00",
"last_ball": 17,
"balls_drawn": 2,
"balls_total": 90
},
"jackpot": "8003.20",
"next_slot": null
}
}
| Field | Type | Description |
|---|---|---|
status |
string | scheduled: no round is running, but a draw is scheduled (next_slot is set). selling: tickets are on sale. drawing: balls are being drawn (a short host pause is included). idle: nothing to show. unavailable: 19flip could not get the state from the game. Hide the banner for both idle and unavailable. |
server_time |
string (ISO 8601, UTC) | 19flip clock. Count down from it, not from the visitor's clock. |
currency |
string | Currency of all amounts. |
round |
object | null | The running round; null for scheduled, idle, unavailable. |
round.number |
integer | Round number for your account in this currency ("draw #15"). |
round.is_demo |
boolean | A demo round played without money. |
round.sale_closes_at |
string | null | When ticket sales close. |
round.players_count / cards_count |
integer | Players with at least one card, and the number of cards sold. |
round.bank |
string (decimal) | Current pot. |
round.last_ball |
integer | null | Last ball shown to players. |
round.balls_drawn / balls_total |
integer | Balls shown so far, out of the total. |
jackpot |
string (decimal) | null | Current top jackpot tier; null if there is no jackpot fund. |
next_slot |
object | null | { "starts_at": "<ISO 8601>", "title": string \| null }: the next scheduled draw. For up to 30 minutes after starts_at, while the host has not opened sales yet, the same slot is still returned, so show "starting soon". |
Amounts are decimal strings, so no precision is lost. The state holds only what any viewer of the round can see. Balls are included only after they are shown to players.
Errors: an unknown key, a disabled game or banner, or a currency you are not allowed to use all return the same 404 NOT_FOUND, with no details. currency must be three letters (400 REQUEST_ERROR otherwise). The error body is the usual one (Errors).
Load and caching¶
- Each open lobby tab makes one small request every 3 seconds (every 15 seconds in a hidden tab).
- 19flip caches the state per account and currency. The game is asked at most once every 2 seconds, however many visitors you have. The public endpoint is also cached for 2 seconds at the edge.
- If the game does not answer, the last known state is shown for up to 60 seconds, and then the banner collapses. Five failed requests in a row in the browser also collapse it. The banner never breaks your page.
embed.jsandframe.htmlare cached for 5 minutes, and the other assets are immutable. New banner versions reach your lobby within minutes, without any change on your side.
Security¶
banner_keyis public by design. It shows the same public data that any viewer of the game sees. Keep yourapi_keyandsecret_keyon your server only.- The iframe runs with
sandbox="allow-scripts allow-same-origin"and cannot navigate your page. Navigation happens only through youronOpenhandler or thedata-click-urlyou set. - The banner pages send no
X-Frame-Optionsorframe-ancestorsrestriction, so you can embed them on any of your domains.
Related pages¶
- Launch game: what to call when the banner is clicked.
- Authentication:
Api-Keyand IP allowlist forGET /api/operator/v1/banners. - Game specifics: how Bingo Bongo rounds work.