Integration Guide
Everything you need to embed games into your website and handle rewards.
Credentials
Used in every iframe tenantCode= parameter.
Use for server-to-server requests. Never expose in client-side code.
Used to verify incoming webhook signatures. Store in an environment variable.
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
| Field | Description |
|---|---|
| session.finalScore | Player's score for this session |
| session.rewardId | Reward earned (null if none) |
| session.userKey | The userKey you passed to the iframe |
| leaderboard.rank | Player's current rank (null if leaderboard disabled) |
| leaderboard.entries | Top 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
| type | When it fires |
|---|---|
| GAME_RESULT | Session submitted — contains score, rewardId, leaderboard rank |
| GAME_BACKLINK | Player tapped the backlink button — contains mode (same_tab / new_tab / close) and url |
| GAME_CLOSE | Player 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')
})