Skip to content

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

  1. You get your public banner_key and a ready embed snippet from GET /api/operator/v1/banners.
  2. embed.js creates an <iframe> served by 19flip inside your container.
  3. 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.
  4. 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.
  5. On click, the iframe sends an open message. embed.js turns it into your onOpen callback 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

GET /api/operator/v1/banners HTTP/1.1
Host: {PROVIDER_HOST}
Api-Key: YOUR_API_KEY

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&currency=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 with border-radius and overflow: hidden on your container.
  • data-19flip-visible is 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:

{ "source": "19flip-banner", "v": 1, "id": "b1", "type": "resize", "payload": { "height": 145 } }
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.

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=USD with the Api-Key header.
  • Public (from a browser, no key): GET /api/public/v1/banners/{banner_key}/{game_id}/state?currency=USD. It has CORS for any origin and Cache-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.js and frame.html are 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_key is public by design. It shows the same public data that any viewer of the game sees. Keep your api_key and secret_key on your server only.
  • The iframe runs with sandbox="allow-scripts allow-same-origin" and cannot navigate your page. Navigation happens only through your onOpen handler or the data-click-url you set.
  • The banner pages send no X-Frame-Options or frame-ancestors restriction, so you can embed them on any of your domains.