Skip to main content

Events

Lil Snack games report what the player is doing so your page can react: track engagement, reveal content after a game finishes, update your own UI.

How you receive those events depends on how the game is embedded, and the two families use different message shapes:

EmbedMessage shape
JavaScript SDK and Direct Endpoint{ type, instanceId, data: { … } }
Game Direct (legacy){ messageType, gameType, gameId, … }

With the SDK you never parse a message yourself; you get typed callbacks.

SDK and Direct Endpoint events​

Games loaded through the SDK or the Direct Endpoint send the same messages.

With the SDK​

Register callbacks and the SDK hands you each event's data directly:

lilsnack.loadGame("GAME_ID", {
target: "#game",
onReady: (instance, data) => console.log("ready", data.hasPlayed),
onStart: () => console.log("started"),
onFinish: (instance, data) => console.log("finished", data.win),
onShare: () => console.log("shared"),
onError: (instance, error) => console.error(error.code, error.message),
});

You can also subscribe globally, which is useful when one page hosts several games:

lilsnack.on("finish", (data, instance) => {
console.log(`${instance.gameId} finished`, data.win);
});

Messages and callbacks​

MessageSDK eventCallbackFires when
lil-snack-game-visitThe game was mounted, before it finished loading.
lil-snack-game-readyreadyonReadyThe game has loaded and is playable.
lil-snack-game-startstartonStartThe player has started playing.
lil-snack-view-how-toThe player opened the instructions.
lil-snack-game-endfinishonFinishThe player has completed the game.
lil-snack-game-shareshareonShareThe player tapped the in-game Share button.
lil-snack-go-homehomeonHomeThe player tapped the in-game Home button. Unity games only, and only when you load with homeButton: true; the game does nothing else on tap.
lil-snack-hint-requestonHintThe player tapped the in-game hint button. Unity games only, and only when you load with enableHints: true. A request, not a notification — see below.
lil-snack-game-errorerroronErrorThe game failed to load or lost its graphics context, or (code GAME_UNAVAILABLE) nothing of a requested format is scheduled.
lil-snack-played-gameRight after lil-snack-game-end, on daily puzzle formats. See below.

Messages without a dedicated callback still reach the SDK's onMessage, which receives the type of every message the game sends.

onHint is the one callback whose return value matters. The game has no purchase UI of its own and waits for your answer: return true (or a promise that resolves to true) once you've charged the player and the hint is revealed; false, a throw, a rejection, or any other value denies it. Without an onHint handler every hint is denied, so pass both enableHints and onHint or neither.

lil-snack-played-game exists for integrations that predate the SDK, and carries win plus a firstWin flag. New integrations can ignore it and use lil-snack-game-end. More generally, ignore any message type you don't recognize: new ones may be added.

Games also send storage requests (lil-snack-get-local-storage, lil-snack-set-local-storage). The SDK answers them for you; with the Direct Endpoint, see Answer storage requests. Hint requests work the same way: the SDK answers lil-snack-hint-request from your onHint; without the SDK you post a lil-snack-hint-response ({ type, requestId, data: { success } }) back into the iframe yourself, echoing the request's requestId.

Event data​

Every lifecycle event carries the same base fields, plus any listed below:

  • gameType: identifies the game's format. Treat it as an opaque string; new formats are added regularly.
  • gameId: the game's identifier.
  • archive: whether this is an archive game.
  • createdAt: ISO timestamp of when the event occurred.
EventAdditional fields
onReadyhasPlayed: whether this player has already played. See Identity and Persistence.
onFinishwin: see the note below. result: the player's answers, on games that produce them. durationActiveSeconds, durationTotalSeconds: seconds since onStart, see the note below.
onErrorcode, message, and not the base fields above.

Three details worth knowing rather than discovering:

  • win is always false for games with no win condition. Polls, tier lists, and similar games report win: false on every completion. Treat win as meaningful only for games you know can be won.
  • result contains the player's raw answers and is only present on games that produce them. Its shape is specific to each game format and can change, so don't build strict parsing against it.
  • durationActiveSeconds counts only active play. Time the page was hidden (another tab or app, a locked screen) or the game was scrolled off screen is left out, so a player who starts, leaves, and comes back later to finish isn't reported as having played the whole time. durationTotalSeconds is the plain wall-clock time from start to finish, away time included.

Raw messages​

You only need this without the SDK, for example in a Direct Endpoint. Each message looks like this:

{
type: "lil-snack-game-end",
instanceId: "game-1", // only when you passed instanceId
data: {
gameType: "tier-list",
gameId: "GAME_ID",
archive: false,
win: false,
durationActiveSeconds: 41,
durationTotalSeconds: 44,
createdAt: "2026-10-06T12:00:00.000Z"
}
}

Note the nesting: the event name is type, and the fields are under data. Messages come from https://staging.play.lilsnack.co (or https://play.lilsnack.co in production); compare event.origin against it exactly.

note

Integrations that predate the SDK may have their partner key configured to receive /play events in the flat Game Direct shape below instead. Your Lil Snack contact can tell you which shape your key receives.

Game Direct events​

Game Direct posts flat messages on window. Every message carries:

  • messageType: the event name, listed below.
  • gameType: identifies the game's format.
  • gameId: unique identifier for the game.
  • archive: whether this is an archive game.
  • createdAt: ISO timestamp of when the event occurred.
messageTypeFires whenAdditional fields
lil-snack-game-visitThe player visits the game.
lil-snack-game-readyThe game has loaded and is ready for interaction.
lil-snack-game-startThe player starts playing.
lil-snack-view-how-toThe player views the "How to Play" instructions.
lil-snack-game-endThe player completes the game.win: whether the player won.
lil-snack-post-game-replayThe player chooses to replay after finishing. Game Direct only.

Example listener​

const LIL_SNACK_ORIGIN = "https://staging.play.lilsnack.co";

useEffect(() => {
const handleGameEvents = (event) => {
// Only accept messages from the Lil Snack host. Compare exactly:
// a substring check also matches hosts that aren't ours.
if (event.origin !== LIL_SNACK_ORIGIN) return;

const { messageType, gameType, gameId, archive } = event.data;

switch (messageType) {
case "lil-snack-game-ready":
console.log("Game ready:", { gameType, gameId, archive });
break;

case "lil-snack-game-visit":
console.log("Game visited:", { gameType, gameId, archive });
break;

case "lil-snack-game-start":
console.log("Game started:", { gameType, gameId, archive });
// Track analytics, update UI, etc.
break;

case "lil-snack-game-end":
console.log("Game ended:", {
gameType,
gameId,
archive,
win: event.data.win,
});
// Update user stats, show rewards, etc.
break;

case "lil-snack-view-how-to":
console.log("User viewed instructions:", { gameType });
break;

case "lil-snack-post-game-replay":
console.log("User replaying game:", { gameType, gameId });
break;
}
};

window.addEventListener("message", handleGameEvents);

// Cleanup listener on component unmount
return () => {
window.removeEventListener("message", handleGameEvents);
};
}, []);

Mobile examples​

In a WebView, the game page is the top-level document, so inject a small script that listens for its messages and forwards them to native code. Messages come from the Lil Snack host itself, so the origin check compares against that host.

iOS (Swift/SwiftUI)​

import SwiftUI
import WebKit

struct LilSnackGameView: UIViewRepresentable {
let partnerId: String
let gameType: String
let onMessageReceived: ((String, [String: Any]) -> Void)?

func makeCoordinator() -> Coordinator {
Coordinator(onMessageReceived: onMessageReceived)
}

// Use https://play.lilsnack.co once your partner key is live in production.
let lilSnackOrigin = "https://staging.play.lilsnack.co"

func makeUIView(context: Context) -> WKWebView {
let contentController = WKUserContentController()

// Inject JavaScript to forward postMessage events to Swift
let script = """
window.addEventListener('message', function(event) {
if (event.origin === '\(lilSnackOrigin)') {
window.webkit.messageHandlers.lilSnackHandler.postMessage(event.data);
}
});
"""

let userScript = WKUserScript(
source: script,
injectionTime: .atDocumentEnd,
forMainFrameOnly: false
)

contentController.addUserScript(userScript)
contentController.add(context.coordinator, name: "lilSnackHandler")

let config = WKWebViewConfiguration()
config.userContentController = contentController
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []

return WKWebView(configuration: config)
}

func updateUIView(_ webView: WKWebView, context: Context) {
let urlString = "\(lilSnackOrigin)/embed-game-direct?partner=\(partnerId)&gt=\(gameType)"
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
webView.load(request)
}
}

class Coordinator: NSObject, WKScriptMessageHandler {
let onMessageReceived: ((String, [String: Any]) -> Void)?

init(onMessageReceived: ((String, [String: Any]) -> Void)?) {
self.onMessageReceived = onMessageReceived
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let messageBody = message.body as? [String: Any],
let messageType = messageBody["messageType"] as? String else { return }

onMessageReceived?(messageType, messageBody)
}
}
}

// Usage
LilSnackGameView(
partnerId: "YOUR_PARTNER_ID",
gameType: "GAME_TYPE",
onMessageReceived: { messageType, data in
switch messageType {
case "lil-snack-game-ready":
print("Game ready")
case "lil-snack-game-start":
print("Game started")
case "lil-snack-game-end":
if let win = data["win"] as? Bool {
print("Game ended - Win: \(win)")
}
case "lil-snack-view-how-to":
print("User viewed instructions")
case "lil-snack-post-game-replay":
print("User replaying game")
default:
break
}
}
)

Android (Kotlin)​

import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import org.json.JSONObject

// Use https://play.lilsnack.co once your partner key is live in production.
const val LIL_SNACK_ORIGIN = "https://staging.play.lilsnack.co"

@Composable
fun LilSnackGameView(
partnerId: String,
gameType: String,
onMessageReceived: (String, JSONObject) -> Unit = { _, _ -> }
) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)

// Inject JavaScript to capture postMessage events
val script = """
window.addEventListener('message', function(event) {
if (event.origin === '$LIL_SNACK_ORIGIN') {
AndroidInterface.handleMessage(JSON.stringify(event.data));
}
});
"""
view?.evaluateJavascript(script, null)
}
}

settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = true
allowContentAccess = true
mediaPlaybackRequiresUserGesture = false
}

// Add JavaScript interface
addJavascriptInterface(
object {
@JavascriptInterface
fun handleMessage(message: String) {
try {
val json = JSONObject(message)
val messageType = json.getString("messageType")
onMessageReceived(messageType, json)
} catch (e: Exception) {
e.printStackTrace()
}
}
},
"AndroidInterface"
)

val url = "$LIL_SNACK_ORIGIN/embed-game-direct?partner=$partnerId&gt=$gameType"
loadUrl(url)
}
}
)
}

// Usage
LilSnackGameView(
partnerId = "YOUR_PARTNER_ID",
gameType = "GAME_TYPE",
onMessageReceived = { messageType, data ->
when (messageType) {
"lil-snack-game-ready" -> {
// Game loaded
println("Game ready")
}
"lil-snack-game-start" -> {
// Game started
println("Game started")
}
"lil-snack-game-end" -> {
val win = data.getBoolean("win")
println("Game ended - Win: $win")
}
"lil-snack-view-how-to" -> {
println("User viewed instructions")
}
"lil-snack-post-game-replay" -> {
println("User replaying game")
}
}
}
)

React Native​

import React from "react";
import { WebView } from "react-native-webview";
import { StyleSheet, View } from "react-native";

// Use https://play.lilsnack.co once your partner key is live in production.
const LIL_SNACK_ORIGIN = "https://staging.play.lilsnack.co";

const LilSnackGameView = ({ partnerId, gameType, onMessageReceived }) => {
const gameUrl = `${LIL_SNACK_ORIGIN}/embed-game-direct?partner=${partnerId}&gt=${gameType}`;

// JavaScript to inject into the WebView
const injectedJavaScript = `
window.addEventListener('message', function(event) {
if (event.origin === '${LIL_SNACK_ORIGIN}') {
window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
}
});
true; // Required for injection to work
`;

const handleMessage = (event) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (onMessageReceived) {
onMessageReceived(data.messageType, data);
}
} catch (error) {
console.error("Error parsing message:", error);
}
};

return (
<View style={styles.container}>
<WebView
source={{ uri: gameUrl }}
style={styles.webview}
injectedJavaScript={injectedJavaScript}
onMessage={handleMessage}
javaScriptEnabled={true}
domStorageEnabled={true}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
/>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
},
webview: {
flex: 1,
},
});

// Usage
const App = () => {
return (
<LilSnackGameView
partnerId="YOUR_PARTNER_ID"
gameType="GAME_TYPE"
onMessageReceived={(messageType, data) => {
switch (messageType) {
case "lil-snack-game-ready":
console.log("Game ready");
break;
case "lil-snack-game-start":
console.log("Game started");
break;
case "lil-snack-game-end":
console.log("Game ended - Win:", data.win);
break;
case "lil-snack-view-how-to":
console.log("User viewed instructions");
break;
case "lil-snack-post-game-replay":
console.log("User replaying game");
break;
}
}}
/>
);
};

export default LilSnackGameView;