OnAim Integration Guide

1. Introduction

OnAim is a gamification as a service platform that enables casinos, betting operators, and other digital businesses to create event-driven player promotions — without custom development for each campaign.

Your system sends player activity events (such as Bet, Win, Registration, Deposit, or custom-defined events) to OnAim in real time.

OnAim processes these events according to your promotion’s configuration — awarding coins, filling progress bars, updating leaderboards, unlocking prizes, and more.

Players access promotions through a Promotion Landing Page, which you embed into your platform via an iframe, allowing a seamless in-platform experience.

To ensure smooth integration:

  • Player identification must be secure and verified.
  • Events must include the required fields.
  • Specific backend endpoints must be available for player info and coin withdrawals.

Note: OnAim operates as a single-tenant platform. Each partner has a dedicated, isolated environment for event ingestion, data processing, and player interactions.
Integration Pack: an AI-ready integration pack accompanies this guide — a machine-readable implementation specification, the event metadata file format with a JSON Schema, and complete examples. Hand it to your development team or AI coding assistant to implement the integration end-to-end.

2. Event Integration

OnAim’s event processing system supports multiple integration channels depending on your infrastructure.

2.1 Supported Integration Methods

Method Description Recommended Use
REST API Send events to OnAim via HTTPS POST requests. Simpler integrations, low to medium traffic
RabbitMQ Publish events to a configured RabbitMQ exchange. Asynchronous event-driven systems
Kafka (AVRO or JSON) Send serialized events to a Kafka topic provided by OnAim. High-volume, distributed systems
Note: These values are indicative and may vary based on deployment and load. Each tenant can request customized throughput limits.

REST API Endpoints

Events are sent to your dedicated OnAim ingestion environment:

Endpoint Body Responses
POST /api/Integrations/events Single event (JSON object) 202 Accepted — 400 if body is empty — 503 if the ingestion buffer is full (retry later)
POST /api/Integrations/events/batch JSON array of events 202 Accepted with {"accepted": N} — 400 if body is not an array — 503 with {"error", "accepted"} on backpressure
Important: 202 Accepted acknowledges receipt only. Events are validated and processed asynchronously after the response is returned (see 2.2). The base URL of your ingestion environment is provided during onboarding.

2.2 Dynamic Event Schema

OnAim accepts fully dynamic event structures.

Core fields:

{
  "customerId": "string (required)",
  "eventType": "string (strongly recommended)",
  "create_time": "string, ISO 8601 (recommended — the event's business time)"
}
  • customerId is the only hard requirement. Events without it are discarded during processing. Alias playerId is also accepted.
  • eventType identifies the event for aggregation rules. Field name is matched case-insensitively (eventType / EventType); alias eventCode is also accepted. If omitted, the event is ingested with a generic default type and will not match your configured event types — always send it.
  • create_time carries the event's actual occurrence time. Do not rely on a timeStamp/timestamp field: the processing pipeline sets timestamp server-side — from create_time when present, otherwise from the server clock at ingestion time. A client-supplied timestamp value is overwritten.

All other fields are free and flexible — you can include any additional data relevant to your use case.

These fields can be:

  • Shared properties, such as `deviceType`, `country`, or `sessionId`
  • Event-specific properties, such as `amount`, `gameName`, or `RefereeId`

Field names are flexible: you don't need to rename anything on your side. OnAim maps your existing property names to the fields used in promotion configuration, and the mapping can be adjusted at any time without changes to your integration (see 2.5).

Validation Behavior

Validation happens asynchronously, after the 202 Accepted response:

  • An event missing customerId is accepted with 202 and then discarded during background processing. The drop is logged on the OnAim side; there is no client-visible rejection signal.
  • Validate payloads on your side before sending — a 202 does not guarantee the event was processable.

Payload Structure

Event payloads must be flat (a single level of key/value pairs, as in the example in 2.3). Nested objects are not flattened — a nested object is stored as one opaque string value and cannot be used in aggregation conditions. You can extend the (flat) event structure anytime without redeploying backend code.

2.3 Example Event

{
    "customerId": "P_123456",
    "eventType": "Bet",
    "create_time": "2025-05-23T10:16:58.980Z",
    "deviceType": "Mobile",
    "gameName": "Roulette",
    "gameType": "Table",
    "amount": 100,
    "currency": "USD"
}
  • `customerId` is mandatory; `eventType` and `create_time` are strongly recommended (see 2.2).
  • `deviceType` is a shared field.
  • `gameName`, `gameType`, `amount`, and `currency` are custom properties.
  • The structure is flat — all properties are accessible in the Admin Panel once described in the schema.

2.4 Event Metadata File

While OnAim accepts any dynamic event, the Admin Panel can only display and filter fields described in your event metadata file — a JSON catalog of your event types, their fields, and known value sets.

OnAim imports this file to generate the rule-building inputs in the Admin Panel (filters, dropdowns, numeric inputs, aggregation targets). Re-imports are idempotent — definitions are updated, never duplicated — so the file can be iterated and re-delivered at any time.

Example:
{
  "events": [
    {
      "code": "Bet",
      "label": "Bet Placed",
      "fields": [
        { "code": "amount",    "label": "Bet Amount",  "type": "decimal", "isAggregateable": true },
        { "code": "currency",  "label": "Currency",    "type": "string",  "isAggregateable": false },
        { "code": "game_code", "label": "Game Code",   "type": "string",  "isAggregateable": false },
        { "code": "channel",   "label": "Bet Channel", "type": "string",  "isAggregateable": false }
      ]
    }
  ],
  "values": [
    {
      "eventCode": "Bet",
      "fieldCode": "channel",
      "stringValues": [
        { "code": "web",    "label": "Web" },
        { "code": "mobile", "label": "Mobile" }
      ]
    }
  ]
}

Metadata File Rules

Rule Description
Coverage Every eventType you send needs an events[] entry; every payload field a fields[] entry whose code exactly matches the JSON key on your events (case-sensitive).
Field types string, int, decimal, dateTime, stringArray, intArray.
isAggregateable true only for numeric fields that promotion rules may aggregate (sums, thresholds) — amounts, values.
Value sets values[] entries (stringValues / intValues) render as dropdown selectors in the Admin Panel. code = raw value as sent; label = display name.
Computed fields The file may declare computedFields rules (game-type lookup, currency conversion) — see 2.5. Declare each rule's output field in fields[] too.
Metadata, not payload This file describes your events; actual payloads are still sent flat as in 2.3.
Extensibility Undeclared fields are still ingested (just not usable in Admin filters); re-import the file anytime to extend it.

Full format specification, JSON Schema, and complete examples: Event Metadata File spec in the integration pack.

2.5 Field Mapping & Currency Handling

Configurable Mapping Rules

It does not matter what your fields are named or how your raw values are encoded — OnAim derives the fields used in promotion configuration via mapping rules managed in the Admin Panel. For example, a gameType rule can classify combinations of game_code + vendor_code as slot or crash, and promotion conditions then target the derived gameType field. Mappings can be added or updated at any time from the Admin Panel, with no changes or redeployment on your side.

Rules can also be declared upfront in your event metadata file via computedFields (rule types Lookup and CurrencyConversion) — see 2.4 and the metadata file spec for the exact format.

Currency Conversion

Currency handling works through the same mapping mechanism, with access to conversion rates. Currency fields on events (e.g. currency next to amount) do not map to OnAim coins — coins are internal promotion entities awarded by promotion configuration. Instead, OnAim derives normalized amount fields (e.g. usdAmount) from your event's amount + currency pair, and aggregation rules can then target the normalized field.

  • Conversion rates are maintained by OnAim (USD-based) and refreshed automatically.
  • Currently supported currencies: USD, EUR, GBP, JPY, CAD, AUD, CHF. Additional currencies can be requested during onboarding.
  • If an event carries an unsupported currency, the converted field is simply skipped — the event itself is still processed.
  • For conversion to work, monetary events must include both an amount field and a currency field.

3. Promotion Page Integration (Iframe + OTT Authentication)

Players interact with promotions through a Promotion Landing Page hosted by OnAim. You can embed this page inside your platform using an iframe.

3.1 Base URL


https://[onaim-landing].io/?promotionId=752&lang=ka&landingPageId=574

3.2 Secure Player Identification (OTT Token)

Each player is identified using a One-Time Token (OTT) generated by your backend.


https://[onaim-landing].io/?promotionId=752&lang=ka&landingPageId=574&ott=80b6a17cc251

OTT Requirements

Property Description
Unique per session Must be generated for one player session only.
Server-side generation Must be created and signed by your backend.
Secure transmission Must be passed over HTTPS only.
Example Iframe:
<iframe src="https://[onaim-landing].io/?promotionId=752&lang=ka&landingPageId=574&ott=80b6a17cc251" width="100%" height="800" frameborder="0" allowfullscreen></iframe>

3.3 Alternative Identification Methods

The OTT (one-time token) flow is the default, but it is not the only option. Player identification can also be implemented as:

  • Back-to-back (server-to-server) call — OnAim calls your backend directly to resolve the player for the current session.
  • Signed JWT — your platform passes a JWT signed with a shared key, which OnAim validates to identify the player.
  • Combined — the methods above can be combined (e.g. JWT plus a back-to-back confirmation call) depending on your security requirements.

The concrete method is agreed during onboarding.

Smart Links: instead of pinning the embed to a fixed promotionId / landingPageId, you can target a named Smart Link slug that OnAim resolves per player at request time — see Section 7.1 for details and examples.

4. Player Info Endpoint

The Player Info Endpoint is used by OnAim to fetch information about a player whenever the promotion landing page or event processing requires it.

There is no strict requirement to use the OTT-based flow. The endpoint can be implemented in any form that your system supports, as long as OnAim can uniquely identify the player and retrieve accurate player details.

For example:

  • You can use the previously described OTT flow for higher security.
  • Or, you can expose a direct authenticated endpoint that takes any identifying token, or session key to resolve the player.

The only requirement is that your endpoint must reliably return the correct player information based on the identifier provided by OnAim.

Example Request


GET https://[yourdomain].com/onaim/player/{ONE_TIME_TOKEN}

The one-time token (or other identifier) can be placed in the URL path or as a query parameter — the endpoint template is configured per integration. OnAim authenticates to your endpoint with HTTP Basic auth using credentials you provide during onboarding.

Example Response

{
  "playerId": "P_123456",
  "username": "JohnDoe",
  "email": "[email protected]",
  "country": "GE"
}

4.3 Field Mapping

Map OnAim's required fields — Id (player identifier) and UserName (display name) — to your backend's property names and provide the mapping as shown below:
{
  "FieldMappings": {
    "default": {
      "Id": "YOUR_PLAYER_ID_PROPERTY",
      "UserName": "YOUR_PLAYER_NAME_PROPERTY"
    }
  }
}
Note: The mapping keys on the left must be exactly Id and UserName. The values on the right are the property names as they appear in your endpoint's JSON response (e.g. "Id": "playerId" for the example response above).

5. Bonus Configuration (Coin Withdrawal)

When players redeem withdrawal coins (coins that represent real or virtual bonuses), OnAim triggers your configured bonus endpoint to process the payout.

All withdrawal options — endpoint URL, HTTP method, headers, query parameters, and body mappings — are fully configured in the OnAim Admin Panel UI.

5.1 Example Configuration

{
  "endpointUrl": "https://api.yourdomain.com/onaim/withdraw",
  "method": "POST",
  "headers": {
    "Authorization": "Bearer {API_KEY}"
  },
  "body": {
    "playerId": "{PlayerId}",
    "quantity": "{Quantity}",
    "value": "{Value}",
    "externalId": "{ExternalId}"
  }
}

Available Placeholders

Body, headers, and query parameters may reference the following placeholders, which OnAim resolves per withdrawal:

Placeholder Description
{ExternalId} Unique id of this withdrawal (transactionId_guid). Use this as your idempotency key — it stays the same across all retries of the same withdrawal.
{PlayerId} / {Username} Player identifier and display name.
{Quantity} / {Value} Number of withdrawal units and the value of one unit.
{PromotionId} / {LandingPageId} / {CampaignCode} Context of the withdrawal (empty when not applicable).
{CurrentDate} UTC timestamp of the request (yyyy-MM-ddTHH:mm:ss).

Request Authentication

In addition to any static headers you configure (e.g. an API key), OnAim can authenticate withdrawal requests with:

  • HTTP Basic auth — credentials agreed during onboarding.
  • HMAC-SHA256 request signing — when enabled, each request carries X-Timestamp (Unix seconds) and X-Signature = Base64(HMAC-SHA256(secret, "{timestamp}.{rawBody}")). Retried requests are re-signed with a fresh timestamp.

5.2 Response Requirements & Behavior

Condition Expected Description
Success HTTP 200 OK Withdrawal confirmed. If the response body is JSON containing an error_code property, it must be 0 — a 200 with a non-zero error_code is treated as a failure (and is not retried).
Client error HTTP 4xx Treated as permanent — not retried. Withdrawal marked as failed; visible in admin logs.
Server error / timeout HTTP 5xx, timeout, network error Automatically retried — up to 5 attempts total with a ~5 second delay between attempts. Every attempt sends the same request with the same {ExternalId}.
Duplicate prevention N/A Retries happen automatically, so your endpoint must be idempotent on {ExternalId}: if you have already processed a given {ExternalId}, return success without paying out again.

5.3 Retry and Failure Handling

  • The player's coin balance is deducted and recorded before the withdrawal request is sent (debit-first). A failed payout does not automatically return coins to the player.
  • Retriable failures (5xx / timeout / network) are retried automatically up to 5 attempts, always with the same {ExternalId}.
  • If all attempts fail, the withdrawal is marked as failed in the Admin Panel, where it can be reviewed and manually retried — a manual retry replays the exact original request (same {ExternalId}), so your idempotency handling covers it too.
Requirement: Implement an idempotent withdrawal endpoint keyed on {ExternalId}. Duplicate deliveries of the same withdrawal must not produce duplicate payouts.

6. Security & Best Practices

  • Always use HTTPS for all communications.
  • Generate OTTs server-side only.
  • Limit OTT validity to 1 minute or less.
  • Always include create_time on events so aggregation uses the event's real occurrence time (see 2.2).
  • Validate every event payload before sending — at minimum customerId and eventType — since ingestion accepts events asynchronously and cannot reject them per-request.
  • Make your bonus (withdrawal) endpoint idempotent on {ExternalId} to prevent duplicate payouts (see 5.2).
  • Implement acknowledgment and retry mechanisms for message broker integrations.
  • Regularly update your event metadata file (see 2.4) to expose new event fields — re-imports are idempotent.
  • Consider adding monitoring dashboards to visualize event flow and error rates.

7. Promotion Page Embedding (Frontend)

Sections 1–6 cover the backend integration (events, player info, withdrawals). This section covers the frontend side: how the Promotion Landing Page is embedded into your site and how it communicates with your page at runtime (login, deposit, balance, resize). It expands on the basic iframe embed in Section 3.

There are two embed shapes, both served from the same renderer. The iframe is the currently recommended integration; the Web Component widget is also supported.

Shape What it is When to use
iframe (recommended) The landing page loaded as a URL inside an <iframe>. Communicates via window.postMessage. Default embed. Broadest host compatibility and cross-origin isolation.
Web Component widget A <onaim-landing-page> custom element loaded from a single JS bundle. Style-isolated (Shadow DOM). Communicates via element methods + DOM events. Embedding directly in your own page: no iframe, no scrollbars, sizes to content automatically (minimum one screen height).

7.1 iframe embedding (recommended)

Embed the landing page as a URL inside an <iframe>, using the base URL and OTT described in Section 3. At runtime the iframe and your page exchange postMessage events (see 7.3 and 7.4).

<iframe
  id="onaim-landing"
  src="https://[onaim-landing].io/?promotionId=752&lang=ka&landingPageId=574&ott=80b6a17cc251"
  style="width:100%; border:none;">
</iframe>

The iframe posts its content height so you can resize it (7.5). Do not give it a viewport-relative height (100vh, %) in this mode. This auto-height behaviour is the default and the recommended setup: the promotion reads as a native part of your page, with no inner scrollbar.

When to use embedMode=inner instead. This parameter turns off auto-height and lets the landing page scroll inside a fixed-size iframe. Use it only when the promotion sits in a container whose size you already control — a full-screen dialog, a modal, or a side drawer. Do not use it for a landing page placed inline in a normal page: it produces a scrollable box inside your page, and you have to guess a height that suits every promotion.

Two behaviour differences to be aware of in this mode: dialogs inside the promotion centre themselves and no longer need the scrollTo command (7.3), but "scroll to section" buttons configured on the promotion will not work, because scrolling happens inside the iframe rather than on your page. Avoid embedMode=inner for promotions that use those buttons.

URL parameters

Parameter Required Description
slug Recommended Smart Link slug provided by OnAim (see below). Takes precedence over promotionId / landingPageId. The player token is also removed from the iframe URL after authentication in this mode.
promotionId Conditional Promotion/campaign id (integer). Use together with landingPageId when you are not using slug. This pins the embed to one fixed landing page.
landingPageId Conditional Landing-page id (integer). Normally paired with promotionId. Passed alone (no slug, no promotionId) it selects a Global Page — a multi-promotion catalog page rather than a single promotion. See Section 9.
ott No Player One-Time Token (see Section 3.2). one-time-token is accepted as an alias. Omit it to render the page for a logged-out visitor.
lang Recommended Language code, e.g. en, ka. If omitted the page defaults to ka (Georgian) — always pass this explicitly. An unknown code falls back to the promotion's first configured language.
embedMode No inner disables auto-height posting and lets the landing page scroll inside a fixed-size iframe. Only for promotions placed in a container you already size yourself, such as a dialog or drawer — see the note above. Leave it out for a normal inline embed.
Unauthenticated embeds are supported. Without ott the page renders its logged-out variant; components configured for signed-in players are hidden and no personalized data is fetched. The expected flow is: embed anonymously → the player clicks a call-to-action → the landing sends login (code 1001, see 7.3) → you sign the player in → you reload the iframe with a fresh ott.

Smart Links (recommended targeting)

A Smart Link is a named slug configured in the OnAim admin panel. Instead of pointing your embed at a specific promotion and landing page, you point it at the slug, and OnAim decides at request time which promotion and landing page that particular player should see — based on the player segment they belong to, with a default for everyone else.

<iframe
  id="onaim-landing"
  src="https://[onaim-landing].io/?slug=summer-promo&lang=en&ott=80b6a17cc251"
  style="width:100%; border:none;">
</iframe>

For you this is a one-parameter change, and it removes ongoing work:

  • Your embed code stops changing. When a campaign ends and a new one replaces it, OnAim repoints the slug — the markup on your site stays exactly as it is. With promotionId / landingPageId you would have to update and redeploy your page for every campaign.
  • Pass the player token if you want segment-based routing. Segments are resolved from the authenticated player, so a visitor embedded without ott always receives the default landing page.
  • One slug can serve different players different pages — VIP, new player, country-specific — with no branching logic on your side.
  • A slug can be deactivated in the admin panel. If that happens the embed will fail to load and the iframe will show an error page; this is a configuration state, not a fault in your integration.

Ask OnAim for the slug that corresponds to your placement. If you have not been given one, use promotionId + landingPageId as shown above.

Player token lifecycle

The OTT is exchanged once, inside the landing page, for a short-lived session. Practical consequences for your integration:

  • There is no token refresh. When the session expires, the landing page does not renew it — reload the iframe with a newly generated OTT.
  • OTTs are single-use and short-lived. Generate one per embed, at the moment you render the iframe. Do not cache it or reuse it across page loads.
  • Failures are silent. If the token is rejected, the page renders as logged-out; no message is sent to your page. If the player unexpectedly appears signed-out, check the token first.
  • With slug, the session is cached briefly in the iframe so a refresh keeps the player signed in. Browsers that block third-party storage (Safari, and Chrome with storage partitioning) will drop that cache — the player is then treated as logged-out on refresh, which is expected. No cookies are used anywhere in the integration.

7.2 Web Component widget

Add the widget bundle and drop the element wherever the promotion should appear. OnAim provides the bundle URL and your promotion-id / landing-page-id values — the widget loads its own environment configuration from the host it is served from, so no additional setup is required on your side.

<!-- widget bundle (URL provided by OnAim) -->
<script type="module" src="https://<LANDING_HOST>/widget/onaim-landing-page.js"></script>

<!-- the element -->
<onaim-landing-page
  promotion-id="752"
  landing-page-id="574"
  language="ka"
  otp="80b6a17cc251">
</onaim-landing-page>
Overriding the gateway (optional). The widget fetches its configuration from https://<LANDING_HOST>/env.js on load. If your Content-Security-Policy blocks that, or you need to point the widget at a specific gateway, define the global yourself before the bundle tag and the widget will use it and skip the fetch:
<script>
  window.OnAim = window.OnAim || {};
  window.OnAim['landing-v2'] = { GATEWAY_URL: 'https://<GATEWAY_HOST>' };
</script>
If neither is available the widget still loads, but its API calls fail and it renders nothing — check the browser console for an [env] message.
The widget adds two tags to your <head>. On mount it injects a Google-Fonts stylesheet link (marked data-onaim-font="roboto"), and — only if your page has none — a <meta name="viewport" content="width=device-width, initial-scale=1"> tag, which the promotion's responsive layout depends on. If your site is not otherwise mobile-responsive, adding that meta tag can change how your page renders on mobile; declare your own viewport meta tag to keep control.

The widget fetches its own configuration, renders inside its Shadow DOM, and grows to fit its content — no height messaging is required. Note that it always occupies at least the full height of the screen, even for a short promotion, so it is designed to be placed as a full section of your page rather than inline between paragraphs.

Widget attributes

Attribute Required Description
promotion-id Yes Promotion/campaign id (integer).
landing-page-id Yes Landing-page id (integer).
language No Language code, e.g. en, ka.
otp No Player One-Time Token for personalized/authenticated content (see Section 3.2).
enable-signalr No Real-time updates. Enabled unless set to "false".
config No Inline configuration JSON (an array of components, or { components, fonts }). Testing/preview only — normally the widget fetches its own configuration.

Methods and events

Call methods directly on the element, and listen for its DOM events:

const el = document.querySelector('onaim-landing-page');

el.setLanguage('en');               // switch language at runtime
// el.setConfig([...]);             // replace config (preview/testing only)

el.addEventListener('ready', () => console.log('landing ready'));
el.addEventListener('error', (e) => console.error('landing error', e.detail));
Important: the widget still uses the postMessage commands in 7.3 for player actions — login (1001), register (1002), openTab (1011) and deposit (1012). Because there is no iframe, the widget posts them to your own page's window, so you must add the same message listener shown in 7.3. Without it, the Login, Register and Deposit buttons inside the widget will do nothing. The iframe-specific messages are height (1003) and scrollTo (1031) — the widget never sends either (it scrolls your page directly).

One exception: a few device-routed external links (for example the refer-a-friend CTA) call window.open() directly in widget mode instead of posting openTab. Those are subject to your browser's popup blocker; they are opened from a real click, so a standard blocker allows them.
// Widget mode: the same handler as 7.3, listening on your own window.
window.addEventListener('message', (event) => {
  const { code, value } = event.data || {};
  switch (code) {
    case 1001: showLoginModal(); break;
    case 1002: showRegisterModal(); break;
    case 1011:
      if (typeof value === 'string' && value.startsWith('https:')) {
        window.open(value, '_blank', 'noopener');
      }
      break;
    case 1012: openDepositFlow(value); break;
  }
});

7.3 Messages FROM the landing page (landing → host)

Add a single message listener on the parent window. This applies to both embed shapes — in the iframe embed the messages come from the iframe, in the widget embed they arrive on your own window (7.2). Command messages carry both a numeric code and a string action (match on either); the message field is a human-readable label and must not be used for control flow.

Handling 1001, 1002, 1011 and 1012 is required for a working integration in both shapes; 1031 is required for the iframe embed (widgets never send it); 1003 is required for the auto-height iframe embed.

code action Extra Meaning / host action
1001 login — Player requested sign-in. Show your login UI; after a successful login, reload the iframe with a fresh OTT.
1002 register — Player requested registration. Show your sign-up UI.
1003 height value (px) Content height changed. Resize the iframe (7.5).
1011 openTab value (URL) Open an external link in a new tab.
1012 deposit value (method or null) Player triggered a deposit. Open your deposit/cashier flow. value is an optional payment-method hint and is currently always null — handle the null case.
1031 scrollTo value (Y px) Required. Scroll your page so this Y offset aligns to the top of the viewport. The landing sends this to bring a dialog into view when it opens, and for any "scroll to section" button configured on the page. The value is already adjusted for a comfortable top margin — apply it as-is, do not re-center it.
1032 scrollLock value (true/false) Recommended, after 1031/1020. A landing dialog opened (true) or the last one closed (false) — freeze and restore your page's scroll. Auto-height embeds only. In auto-height mode the iframe never scrolls, so the landing cannot hold your page still by itself; without this the page keeps scrolling behind an open dialog. Ref-counted and always balanced (overlapping dialogs send one true and one false), plus an unlock on pagehide. See the note below for the prerequisite and the iOS Safari variant.
1021 host-viewport-request — Optional. The landing is asking for your current viewport (7.7) — sent when a pinned component mounts or a dialog opens. You can receive it more than once per session; answer each one by posting a single host-viewport message. Ignore it if you have not implemented 7.7.
scrollLock (1032) on iOS Safari. iOS ignores overflow: hidden on the body, so pin the scroll position instead: on lock store window.scrollY, set body.style.position = 'fixed' and body.style.top = '-' + storedY + 'px'; on unlock clear both and window.scrollTo(0, storedY). Note that scrollTo (1031) stays live while locked — the landing sends 1032 after any 1031 that positions the dialog, but scroll-to-section buttons fire at any time; with the pinned variant apply an incoming 1031 by updating the stored Y and body.style.top, because a locked page ignores window.scrollTo. Handle scrollTo (1031) or host-viewport (1020) before implementing 1032. Those position a dialog on the visible screen; 1032 only freezes the page. Locking without either leaves the dialog off-screen with no way for the player to scroll to it, which reads as a frozen landing. Skipping 1032 is safe — without it the page simply scrolls behind an open dialog, as it does today.
Dialogs and popups depend on 1031 or host-viewport (7.7). In the auto-height iframe embed the iframe is as tall as its content and never scrolls — your page does. If you implement host-viewport (code 1020, section 7.7), dialogs render centered on the player's visible screen and 1031 is no longer sent for dialogs. Otherwise, when a dialog opens the landing asks you to scroll it into view via 1031. If you implement neither, dialogs will open far outside the visible screen and appear broken. Keep the 1031 handler in every case — "scroll to section" buttons post it in any iframe embed, regardless of host-viewport.

State messages use type (no code). They are reserved for upcoming promotion types and are not emitted by the current release — you can safely omit handlers for them today; they are listed so you can plan for them.

type Extra Meaning
balance-update amount (number) In-game balance changed. Refresh your balance display.
no-more-balance — Player is out of balance. Prompt a deposit.
tournament-play — Tournament-over "Play" CTA. Route the player to the next playable surface.
Native app embeds: inside the OnAim native app, the deposit (1012) and a link / redirect command (code 1010, with value = URL) are delivered through window.PromotionChannel instead of postMessage. Pure web hosts only need the postMessage codes above.

Complete listener:

const iframe = document.getElementById('onaim-landing');

window.addEventListener('message', (event) => {
  // Required in production: only act on messages from the landing page.
  // iframe embed — the message comes from the landing origin:
  if (event.origin !== 'https://<LANDING_HOST>') return;
  // Widget embed — the widget runs inside your page, so the message comes from
  // your own origin instead. Use this check there:
  // if (event.origin !== window.location.origin) return;

  const { type, code, value, amount } = event.data || {};

  switch (code) {
    case 1001: showLoginModal(); break;                       // login
    case 1002: showRegisterModal(); break;                    // register
    case 1003: iframe.style.height = value + 'px'; break;     // height
    case 1011:                                                // openTab
      if (typeof value === 'string' && value.startsWith('https:')) {
        window.open(value, '_blank', 'noopener');
      }
      break;
    case 1012: openDepositFlow(value); break;                 // deposit (value is currently always null)
    case 1031: { // scrollTo — required; value is a Y offset in the IFRAME's document
      const r = iframe.getBoundingClientRect();
      window.scrollTo({ top: r.top + window.scrollY + value, behavior: 'smooth' });
      break;
    }
    case 1032: // scrollLock — hold your page still behind a landing dialog
      // iOS Safari ignores overflow:hidden on body; pin scrollY instead (see note above).
      document.body.style.overflow = value ? 'hidden' : '';
      break;
    case 1021: postHostViewport(); break; // host-viewport-request — see 7.7
  }

  // Reserved, not emitted by the current release:
  switch (type) {
    case 'balance-update':  updateBalance(amount); break;
    case 'no-more-balance': showDepositPrompt();   break;
    case 'tournament-play': routeToNextGame();     break;
  }
});
Security: treat every value URL as untrusted — allow only https: links and open them with noopener. Validating event.origin is your responsibility: the landing page broadcasts its messages and does not restrict who receives them, so without this check any other frame on your page could impersonate it.

7.4 Messages TO the landing page (host → iframe)

Post to the iframe's contentWindow:

const iframe = document.getElementById('onaim-landing');
iframe.contentWindow.postMessage({ type: 'SET_LANGUAGE', payload: 'en' }, '*');
type Payload Effect
SET_LANGUAGE string Switch the display language.
SET_LANGUAGES string[] Adds languages to the list already available. This is additive only — languages cannot be removed by sending a shorter list.
SET_CONFIG config object/array Replace the rendered component configuration.
SET_GLOBAL_VARIABLES object (CSS variables) ⚠️ Not currently applied in iframe mode. For widget theming, use the element's setGlobalVariables() method instead.
host-viewport (action, code 1020) scrollY, innerHeight, iframeTop Pinned components + dialog centering — see 7.7.

7.5 Height auto-adjustment (iframe)

Unless you pass embedMode=inner, the iframe measures its content and posts a height command (code 1003) whenever it changes (initial load, dynamic content, layout shifts). Apply it so there is no inner scrollbar:

window.addEventListener('message', (event) => {
  const { code, value } = event.data || {};
  if (code === 1003) {
    document.getElementById('onaim-landing').style.height = value + 'px';
  }
});

A CSS transition: height 0.2s on the iframe keeps resizes smooth.

The height is re-checked frequently, so expect to receive this message several times per second, often with an unchanged value. Ignore no-op updates rather than assuming one message per real change:

let lastHeight = 0;
if (code === 1003 && Math.abs(value - lastHeight) > 1) {
  lastHeight = value;
  iframe.style.height = value + 'px';
}
If the iframe stops growing: the landing page detects a runaway resize loop (its content growing because the iframe grew, and vice versa) and stops posting heights to protect your page. The symptom is an iframe that freezes at one height with content cut off; it starts posting again by itself once the content genuinely shrinks. It is almost always caused by giving the iframe a viewport-relative height — see the warning below.
Do not give the iframe a viewport-relative height (100vh, %) in auto-height mode: the content grows with the iframe, which grows with the content — an unbounded loop. Let the posted height drive the size. (embedMode=inner avoids the loop too, but it is only appropriate for dialog-style containers — see 7.1.)

7.6 iframe vs widget

Concern iframe (recommended) Web Component widget
Embed <iframe src> <onaim-landing-page> + bundle
Style isolation full document boundary Shadow DOM
Host → landing postMessage (SET_LANGUAGE, SET_CONFIG) element methods (setLanguage, setConfig)
Landing → host postMessage codes (7.3) postMessage codes (7.3, on your own window) + DOM events (ready, error)
Height height 1003 → you resize (7.5) grows automatically
Targeting promotionId + landingPageId (URL) promotion-id + landing-page-id (attributes)
Player token ott URL parameter otp attribute
Theming not applied via postMessage setGlobalVariables() method
Player actions (login / register / deposit) postMessage codes (7.3) postMessage codes (7.3) — same handler, listening on your own window
Targeting by player segment Smart Link slug (7.1) not available — use fixed ids

7.7 Sharing your scroll position with the iframe (host viewport, code 1020)

Recommended for the default auto-height iframe embed, and required if your promotion uses pinned / sticky components — components configured to stay fixed to the top or bottom of the screen while the player scrolls. It also gives dialogs and popups proper in-place centering: they render on the player's visible screen instead of the page jumping to them (1031). By posting your page's scroll position and viewport size down to the iframe, you let the landing page see where the visible screen is.

Why it's needed: in auto-height mode the iframe is grown to its full content height (7.5), so the iframe itself never scrolls — your page does. Code inside the iframe cannot read your scroll position across origins, so on its own it can only position things relative to the whole (tall) document, not the player's visible screen. The host-viewport message tells it where that visible screen is.

Skipping it degrades gracefully rather than breaking: a pinned component simply scrolls with the page instead of staying fixed, and dialogs fall back to the scrollTo page jump (1031, see 7.3). Implement it if your promotion uses pinned components or you want dialogs centred without a page jump. The Web Component widget and self-sized iframes (embedMode=inner) have a real viewport of their own, so they do not need it — the landing ignores host-viewport in those modes.

Dialog centering: when you post host-viewport, dialogs and popups render centered on the player's visible screen and the landing stops sending scrollTo (1031) for dialogs. Keep the 1031 handler anyway — "scroll to section" buttons still post it, and it is the dialog fallback for any moment the landing has not yet received a viewport from you. The landing ignores host-viewport outside the auto-height embed (e.g. embedMode=inner), so posting it unconditionally is safe.

Post your viewport to the iframe

Send a host-viewport message to the iframe's contentWindow on load, on scroll, and on resize:

Field Type Value
action string "host-viewport"
code number 1020
scrollY number Current vertical scroll position of your page, in px (window.scrollY).
innerHeight number Height of the visible viewport, in px (window.innerHeight).
iframeTop number The iframe element's top edge in your document: rect.top + window.scrollY.

All three numeric fields are required and must be finite numbers — a malformed message is ignored and the last good value is kept.

const iframe = document.getElementById('onaim-landing');

function postHostViewport() {
  const rect = iframe.getBoundingClientRect();
  iframe.contentWindow.postMessage({
    action: 'host-viewport',
    code: 1020,
    scrollY: window.scrollY,
    innerHeight: window.innerHeight,
    iframeTop: rect.top + window.scrollY,   // iframe's top in the document
  }, '*');
}

// Post on scroll and resize (passive listeners keep scrolling smooth)…
window.addEventListener('scroll', postHostViewport, { passive: true });
window.addEventListener('resize', postHostViewport);

// …and once when the iframe is ready, so overlays are placed correctly before any scroll.
iframe.addEventListener('load', postHostViewport);

// The iframe may ask for the current viewport (action 'host-viewport-request',
// code 1021) — when a pinned component mounts or a dialog opens. It can arrive
// more than once per session; answer each by posting once. Post from THIS
// window (the iframe's direct parent) — other frames are ignored.
window.addEventListener('message', (event) => {
  if (event.source === iframe.contentWindow && event.data?.code === 1021) {
    postHostViewport();
  }
});
Tip: keep posting on every scroll and resize — the landing coalesces the updates to one per animation frame. Also re-post after any layout change that moves the iframe without a scroll (content collapsing above it, applying a new height from a 1003 message), or iframeTop goes stale and overlays land offset.

7.8 Network and Content-Security-Policy requirements

The landing page talks to OnAim services directly from the player's browser. If your site sends a Content-Security-Policy header, or sits behind a proxy that filters outbound traffic, allow the following. <LANDING_HOST>, <GATEWAY_HOST>, <ASSET_HOST>, <GAME_HOST> and <WIDGET_HOST> are provided by OnAim per environment.

How much of this applies depends on the embed shape. In the iframe embed the landing page runs in its own document under its CSP, so your page only needs frame-src (plus connect-src if a proxy filters outbound traffic). In the Web Component widget and the floating widget the OnAim code runs inside your document, so your CSP governs everything it loads — every row below applies.
Directive Allow Needed for
frame-src <LANDING_HOST> iframe embed (7.1), and the dialog opened by the floating widget (Section 8)
script-src, img-src, font-src <LANDING_HOST> Web Component widget — the bundle, its env.js configuration, its images and its fonts are loaded from the OnAim host into your page. script-src covers both the bundle and env.js; if you would rather not allow the latter, define the gateway global yourself (7.2).
img-src, media-src <ASSET_HOST> Widget — images, videos and GIFs uploaded in the admin panel are served from OnAim's file/CDN host, not from <LANDING_HOST>
script-src, connect-src <GAME_HOST> Widget — promotions containing a game load the game as a script into the page. Blocked = the game silently never starts.
frame-src, img-src https://www.youtube.com, https://img.youtube.com Widget — promotions containing a YouTube video component
style-src, font-src https://fonts.googleapis.com, https://fonts.gstatic.com Web Component widget and floating widget — web fonts
connect-src <GATEWAY_HOST> Promotion data, balances, player eligibility
connect-src (WebSocket) wss://<GATEWAY_HOST> Real-time updates (balance, leaderboards)
script-src, connect-src <WIDGET_HOST>, <WIDGET_API_HOST> Floating promotion widget (Section 8) — embed.js and its configuration script are served from <WIDGET_HOST>; the widget's own configuration API is a separate host from the gateway
WebSockets must not be blocked. Leaderboards connect over a raw WebSocket with no HTTP fallback. If a corporate proxy or firewall blocks the upgrade, leaderboards silently fall back to slow periodic refreshes instead of updating live — this is a common cause of "leaderboard looks frozen" reports.

7.9 Troubleshooting

Symptom Likely cause
The page renders in Georgian. No lang parameter / language attribute was passed — it defaults to ka (7.1).
Login / Register / Deposit buttons do nothing. No message listener on your page (7.3). This applies to the widget embed too.
A dialog opens but the player cannot see it. Neither scrollTo (1031, 7.3) nor host-viewport (1020, 7.7) is handled. If you do post host-viewport, check that iframeTop is rect.top + window.scrollY — posting the raw rect.top misplaces dialogs by your current scroll amount.
A pinned component scrolls away instead of staying fixed. host-viewport (1020) is not posted, or is posted from a window other than the iframe's direct parent (7.7).
The player appears logged out despite passing a token. Expired, reused or invalid OTT — generate a fresh one per embed (7.1). Rejected tokens fail silently.
The iframe stops growing and content is cut off. Runaway-resize protection triggered, almost always from a viewport-relative iframe height (7.5).
The Web Component widget renders nothing and the console shows an [env] error. The widget could not load env.js from <LANDING_HOST> — usually a script-src CSP rule (7.8). Define the gateway global yourself as a workaround (7.2).
Widget renders, but the game inside a promotion never starts, or images/videos are missing. Your page's Content-Security-Policy is blocking the game script or the asset host (7.8).
Nothing renders at all, and the iframe URL changes to /error. The promotion or landing page could not be loaded — the Smart Link slug may have been deactivated, or the promotionId / landingPageId values are wrong. Check with OnAim.
Blank area where the page should be, no error. The embed URL is missing a valid target (no slug, and no promotionId + landingPageId), or the player is not eligible for this promotion.
Balances or leaderboards do not update live. WebSocket traffic is blocked (7.8).

8. Floating Promotion Widget

The floating widget is a small circular button that sits in the corner of every page of your site, follows the player as they browse, shows their current promotion balance, and opens the full promotion landing page in a dialog when clicked. It is a separate, self-contained product from the embeds in Section 7 — you install it once, site-wide, instead of placing a landing page on one specific page.

Each widget is created and configured in the OnAim admin panel (promotion, coin, landing page, trigger). The admin panel then gives you a ready-made snippet containing your widget id — copy it from the widget's Embed dialog.

8.1 Installing the widget

Choose one of the three snippets below — they are equivalent. All of them need your widget id and the player's One-Time Token (Section 3.2). Install the snippet on every page where the widget should appear, ideally in your global layout.

Method 1 — script tag

Simplest option for server-rendered sites. The widget creates its own container and attaches itself to the page.

<script
  data-balance-widget
  data-widget-id="YOUR_WIDGET_ID"
  data-player-token="PLAYER_OTT"
  data-lang="en"
  src="https://<WIDGET_HOST>/embed.js">
</script>

Method 2 — custom element

Preferred for single-page applications: you control exactly where the element lives and can update its attributes as your app state changes.

<script type="module" src="https://<WIDGET_HOST>/embed.js"></script>

<minimal-widget
  widget-id="YOUR_WIDGET_ID"
  player-token="PLAYER_OTT"
  lang="en">
</minimal-widget>

Method 3 — programmatic loader

Use when you cannot add markup directly, for example when injecting the widget from a tag manager.

(function () {
  const s = document.createElement('script');
  s.type = 'module';
  s.src = 'https://<WIDGET_HOST>/embed.js?' + Date.now();
  s.async = true;
  s.setAttribute('data-balance-widget', '');
  s.setAttribute('data-widget-id', 'YOUR_WIDGET_ID');
  s.setAttribute('data-player-token', 'PLAYER_OTT');
  s.setAttribute('data-lang', 'en');
  document.body.appendChild(s);
})();
No configuration is required on your side. The widget loads its own environment settings from the OnAim host it is served from.

8.2 Attributes

Attribute (element) Attribute (script tag) Default Description
widget-id data-widget-id — Required. Copied from the widget's Embed dialog in the admin panel.
player-token data-player-token — Required. The player's One-Time Token (Section 3.2). Also forwarded to the landing page when the dialog opens, so the player stays signed in.
lang data-lang en Language code, e.g. en, ka.
progress-mode data-progress-mode cumulative How progress is displayed: cumulative or step.

Changing lang or progress-mode updates the widget in place. Changing widget-id or player-token re-initialises it, including re-authenticating the player — so set a fresh token by updating the attribute rather than re-injecting the script.

8.3 Open / close events

The widget notifies your page when the promotion dialog opens and closes, so you can pause background video, hide your own overlays, or record analytics. Events carry { promotionId } in event.detail.

// Custom element (Method 2)
const el = document.querySelector('minimal-widget');
el.addEventListener('minimal-widget:open',  (e) => console.log('opened', e.detail));
el.addEventListener('minimal-widget:close', (e) => console.log('closed', e.detail));

// Script tag (Methods 1 and 3) — the widget owns its container, so listen on document
document.addEventListener('minimal-widget:open',  (e) => console.log('opened', e.detail));
document.addEventListener('minimal-widget:close', (e) => console.log('closed', e.detail));

8.4 What your page still has to handle

The dialog opened by the widget contains the promotion landing page, so the message listener from 7.3 is still required — login, register, deposit and open-link requests made from inside the dialog are delivered to your page exactly as described there. Add that listener once, globally, alongside the widget snippet.

The dialog is a self-sizing embed. The widget loads the landing page with embedMode=inner (7.1), because the dialog already has a fixed size. Two consequences: you never need to handle height (1003) or scrollTo (1031) for the dialog, and — as in any inner embed — "scroll to section" buttons configured on that promotion will not work inside the widget dialog. Pick a landing page without them for this placement.

8.5 Behaviour and limitations

  • Placement. The widget appears in the bottom-right corner and the player can drag it anywhere on the screen; its position is remembered. It stays within the visible area when the browser is resized.
  • Style isolation. The widget renders inside a Shadow DOM, so your CSS cannot affect it and its CSS cannot affect your site.
  • Eligibility. If the player is not eligible for the promotion, the widget renders nothing at all. A missing widget is expected behaviour, not an error.
  • Single-page apps. The widget survives client-side navigation, and installing the same widget id twice is a no-op — it will not appear more than once.
  • Live balance. The balance shown on the button updates in real time over a WebSocket; see the network requirements in 7.8, including the widget-specific hosts.

9. Promotion Hub (Global Pages)

A Global Page — also called a Hub Page — is a single page presenting all of your active promotions as one catalog, rather than one landing page for one promotion. It is embedded as an iframe, like Section 7.1, but it is served by a separate renderer with a smaller contract. Ask OnAim which host and landingPageId to use for your Global Page.

9.1 Embedding

<iframe
  id="onaim-hub"
  src="https://<HUB_HOST>/?landingPageId=574&lang=en&ott=80b6a17cc251"
  style="width:100%; height:1200px; border:none;">
</iframe>
Parameter Required Description
landingPageId Yes Global Page id (integer), provided by OnAim. There is no promotionId — a Global Page spans many promotions.
lang Recommended Language code, e.g. en, ka. Defaults to en here (not ka as in 7.1).
ott No Player One-Time Token (Section 3.2); one-time-token is accepted as an alias. Omit it to render the page for a logged-out visitor. Same single-use, no-refresh lifecycle as 7.1.
Smart Link slugs and embedMode do not apply here. A Global Page is targeted by id only.

9.2 What differs from a promotion landing page

Concern Promotion landing page (Section 7) Global Page (this section)
Embed shapes iframe or Web Component widget iframe only — there is no widget bundle
Height Auto-height by default: the page posts height (1003) and you resize (7.5) No height messages are sent. Give the iframe a height you control, and re-check it on your own breakpoints. The page scrolls internally.
Messages to your page 1001, 1002, 1003, 1011, 1012, 1031 Only login (1001), register (1002) and openTab (1011). No deposit, no height, no scrollTo.
Host viewport (7.7) Optional; required for pinned components Not used — the page has a real viewport of its own
Targeting slug, or promotionId + landingPageId landingPageId only

The message listener from 7.3 is still required — handling 1001, 1002 and 1011 is enough for a Global Page, and the same listener covers both embeds if you use them on one site.

9.3 Network requirements

Because a Global Page is iframe-only, your page's Content-Security-Policy only needs frame-src <HUB_HOST>. The renderer talks to the OnAim gateway from inside its own document. If a proxy filters outbound traffic, apply the connect-src rows of 7.8.