Integration Guide

Everything you need to embed games into your website and handle rewards.

1
Copy credentials
Grab your Tenant Code and Webhook Secret below.
2
Whitelist your domain
Add your site URL to Allowed Domains in Settings.
3
Embed the iframe
Paste the generated snippet into your page.
4
Handle results
Listen for postMessage events and verify webhooks.

Credentials

Tenant Code

Used in every iframe tenantCode= parameter.

API Key

Use for server-to-server requests. Never expose in client-side code.

Webhook Secret (keep private)

Used to verify incoming webhook signatures. Store in an environment variable.

Webhook Endpoint (your server)

Gamifly posts game results here. Set it in Settings.

Allowed domains

The iframe will only load on domains you've whitelisted. Add your production domain in Settings → Allowed Domains.

No domains configured — the game will not load on any external site.

Embed Code Generator

Only active campaigns appear here

Your platform's user identifier variable

<iframe
  src="https://gamifly-api.bts.id/embed?tenantCode=YOUR_TENANT_CODE&gameCode=GAME_CODE&userKey=USER_ID_HERE"
  width="100%"
  height="640"
  frameborder="0"
  allow="autoplay; fullscreen"
  style="border-radius: 12px;"
></iframe>

userKey — a stable, unique identifier for the player in your system (e.g. user ID, email hash, loyalty number). The same key must be used every time that user plays so scores and rewards are attributed correctly. Max 128 characters.

Anonymous / guest visitors — if your site allows playing without a login, generate a random ID and persist it in localStorage so the same browser always gets the same key. Never use a shared "guest" string — all anonymous users would collide on the leaderboard and fraud detection.

// Paste once on every page that embeds a game
function getOrCreateVisitorId() {
  const KEY = 'gf_visitor_id'
  let id = localStorage.getItem(KEY)
  if (!id) {
    const buf = new Uint8Array(12)
    crypto.getRandomValues(buf)
    id = 'v_' + Array.from(buf).map(b => b.toString(16).padStart(2,'0')).join('')
    localStorage.setItem(KEY, id)
  }
  return id
}
// Then use it as the userKey:
// var userKey = isLoggedIn ? currentUser.id : getOrCreateVisitorId()

Height — default 640 px. Adjust to match your game's aspect ratio. The game auto-scales inside the iframe.

variantId — ties the session to a specific campaign's reward rules. If omitted, the latest active variant for that game is used automatically.

Platform Integration Examples

Paste directly into any HTML page. Replace getCurrentUserId() with however your site exposes the logged-in user.

<!-- Gamifly Game Embed -->
<div id="gamifly-container">
  <iframe id="gamifly-frame"
    src=""
    width="100%" height="640"
    frameborder="0" allow="autoplay; fullscreen"
    style="border-radius:12px;"
  ></iframe>
</div>
<script>
  // Replace this with however your site exposes the current user's ID
  var userId = getCurrentUserId() || 'guest_' + Math.random().toString(36).slice(2)
  var url = 'https://gamifly-api.bts.id/embed'
         + '?tenantCode=YOUR_TENANT_CODE'
         + '&gameCode=GAME_CODE'
         + '&userKey=' + encodeURIComponent(userId)
  document.getElementById('gamifly-frame').src = url
</script>

Listen for Game Result (postMessage)

The iframe fires a message event when the player finishes. Use it for instant UI feedback — pop a modal, show the score, trigger confetti. Reward fulfillment should still be handled server-side via webhook.

window.addEventListener('message', (event) => {
  // Always verify origin in production:
  // if (event.origin !== 'https://your-gamifly-api-domain.com') return

  if (event.data?.type !== 'GAME_RESULT') return

  const { session, leaderboard } = event.data

  console.log('User:', session.userKey)
  console.log('Score:', session.finalScore)
  console.log('Reward:', session.rewardId)   // null if no reward
  console.log('Rank:', leaderboard?.rank)    // null if leaderboard disabled

  if (session.rewardId) {
    // Use postMessage for immediate UI feedback only.
    // The authoritative reward grant happens via webhook on your server.
    showRewardModal({ rewardId: session.rewardId, score: session.finalScore })
  }
})

Event payload fields

FieldDescription
session.finalScorePlayer's score for this session
session.rewardIdReward earned (null if none)
session.userKeyThe userKey you passed to the iframe
leaderboard.rankPlayer's current rank (null if leaderboard disabled)
leaderboard.entriesTop entries array

Mobile App Integration (Flutter / Android / iOS)

Load the embed URL in a native webview — the game runs entirely inside it. The embed page fires postMessage events you can intercept natively for results, backlink navigation, and window-close requests.

postMessage events sent by the embed page

typeWhen it fires
GAME_RESULTSession submitted — contains score, rewardId, leaderboard rank
GAME_BACKLINKPlayer tapped the backlink button — contains mode (same_tab / new_tab / close) and url
GAME_CLOSEPlayer tapped a "close" backlink — your app should dismiss/pop the webview screen

Use webview_flutter with a JavascriptChannel named GamiflyBridge. The embed page's window.parent.postMessage arrives in the channel's onMessageReceived callback.

// pubspec.yaml dependency: webview_flutter: ^4.x
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class GameScreen extends StatefulWidget {
  const GameScreen({super.key});
  @override State<GameScreen> createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'GamiflyBridge',      // must match window.parent postMessage target
        onMessageReceived: (msg) {
          final data = jsonDecode(msg.message) as Map<String, dynamic>;
          _handleEvent(data);
        },
      )
      ..loadRequest(Uri.parse(
        'https://gamifly-api.bts.id/embed'
        '?tenantCode=YOUR_TENANT_CODE'
        '&gameCode=GAME_CODE'
        '&userKey=PLAYER_ID',
      ));
  }

  void _handleEvent(Map<String, dynamic> data) {
    switch (data['type']) {
      case 'GAME_RESULT':
        final score   = data['session']['finalScore'];
        final rewardId = data['session']['rewardId'];
        // Show your own result UI or grant reward via your backend
        debugPrint('Score: $score  Reward: $rewardId');
        break;
      case 'GAME_BACKLINK':
        // Player tapped the backlink button — handle navigation natively
        final url = data['url'] as String?;
        if (url != null && url.isNotEmpty) {
          // e.g. open in-app browser or push a new route
        } else {
          Navigator.of(context).pop(); // 'close' mode
        }
        break;
      case 'GAME_CLOSE':
        Navigator.of(context).pop();
        break;
    }
  }

  @override
  Widget build(BuildContext context) =>
    Scaffold(body: WebViewWidget(controller: _controller));
}

// The embed page uses window.parent.postMessage — Flutter's JavascriptChannel
// intercepts those messages automatically when the channel name matches.
// If your embed is inside a nested iframe, you may need to inject a small
// script that forwards window.top.postMessage to GamiflyBridge.postMessage().

Webhook Payload

Gamifly POSTs to your webhook endpoint every time a session completes. This is the authoritative signal — grant rewards here, not from postMessage.

POST https://your-app.com/webhook/gamifly
Content-Type: application/json
X-Gamifly-Signature: sha256=<hmac-sha256(webhookSecret, rawBody)>

{
  "eventId":    "evt_a1b2c3d4e5f6",
  "tenantCode": "YOUR_CODE",
  "userKey":    "user_42",
  "gameCode":   "ski-runner",
  "sessionId":  "<uuid>",
  "score":      1240,
  "rewardId":   "REWARD_GOLD",  // null if no reward earned
  "cheatFlags": [],
  "timestamp":  "2026-07-21T02:13:28.092Z"
}

Signature header: X-Gamifly-Signature: sha256=<hmac-sha256(webhookSecret, rawBody)>

Webhook Signature Verification

Always verify X-Gamifly-Signature before processing a reward. Use a timing-safe comparison to prevent timing attacks.

const crypto = require('crypto')
const WEBHOOK_SECRET = process.env.GAMIFLY_WEBHOOK_SECRET
// = 'your-secret-here'

app.post('/webhook/gamifly', express.raw({ type: 'application/json' }), (req, res) => {
  const sig      = req.headers['x-gamifly-signature']   // "sha256=abc123..."
  const expected = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.body)   // raw Buffer — do NOT parse JSON first
    .digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature')
  }

  const payload = JSON.parse(req.body)
  if (payload.rewardId) {
    grantReward(payload.userKey, payload.rewardId)
  }
  res.status(200).send('ok')
})