Web / JavaScript SDK Setup Guide
---
sidebar_label: Web / JavaScript sidebar_category: Gaming SDK Guides
Web / JavaScript SDK Setup Guide
This guide covers integrating Gaming Intelligence into web games and browser-based apps using JavaScript or TypeScript. There is no npm package to install — integration uses the browser's built-in fetch API (or Node.js 18+ fetch) to call the Gaming Intelligence REST API directly. This keeps your bundle size at zero and works in any modern browser or Node.js environment.
Prerequisites
Before you begin, have the following ready:
| Requirement | Where to Find It |
|---|---|
| SDK Key | Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key |
| Organization ID | Linkzly Console → Organization Settings |
| Game ID | Linkzly Console → Gaming → [Your Game] → Settings → General Settings |
| Browser | Any modern browser supporting ES2017+ and the fetch API |
| Node.js | Node.js 18+ if running server-side (includes native fetch) |
Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.
Setup
Create a LinkzlyGamingClient class that wraps the ingestion endpoint. This is the only file you need to add to your project:
// linkzly-gaming.js
class LinkzlyGamingClient {
constructor({ sdkKey, organizationId, gameId, baseUrl = 'https://gaming.linkzly.com' }) {
this.sdkKey = sdkKey;
this.organizationId = organizationId;
this.gameId = gameId;
this.baseUrl = baseUrl;
}
/**
* Send an array of events to the Linkzly ingestion endpoint.
* @param {Object[]} events - Array of event objects.
* @returns {Promise<Object>} The 202 batch response.
*/
async sendEvents(events) {
const payload = {
batch_id: crypto.randomUUID(),
batch_timestamp: new Date().toISOString(),
events,
};
const response = await fetch(`${this.baseUrl}/api/v1/gaming/events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.sdkKey}`,
'X-Organization-ID': this.organizationId,
'X-Game-ID': this.gameId,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Linkzly ingestion error ${response.status}: ${text}`);
}
return response.json(); // BatchResponse
}
}
Instantiate the client once and reuse it throughout your app:
const gaming = new LinkzlyGamingClient({
sdkKey: 'YOUR_SDK_KEY',
organizationId: 'YOUR_ORG_ID',
gameId: 'YOUR_GAME_ID',
});
TypeScript
If your project uses TypeScript, add these interfaces to benefit from type checking:
// linkzly-gaming.ts
interface EventData {
[key: string]: string | number | boolean | null | EventData;
}
interface GameEvent {
event_id: string;
event_type: string;
timestamp: string; // ISO 8601
platform: string;
player_id: string;
session_id: string;
data?: EventData;
}
interface BatchResponse {
batch_id: string;
events_received: number;
events_valid: number;
events_dropped: number;
trace_id: string;
server_timestamp: string;
}
class LinkzlyGamingClient {
private sdkKey: string;
private organizationId: string;
private gameId: string;
private baseUrl: string;
constructor(options: {
sdkKey: string;
organizationId: string;
gameId: string;
baseUrl?: string;
}) {
this.sdkKey = options.sdkKey;
this.organizationId = options.organizationId;
this.gameId = options.gameId;
this.baseUrl = options.baseUrl ?? 'https://gaming.linkzly.com';
}
async sendEvents(events: GameEvent[]): Promise<BatchResponse> {
const payload = {
batch_id: crypto.randomUUID(),
batch_timestamp: new Date().toISOString(),
events,
};
const response = await fetch(`${this.baseUrl}/api/v1/gaming/events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.sdkKey}`,
'X-Organization-ID': this.organizationId,
'X-Game-ID': this.gameId,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Linkzly ingestion error ${response.status}: ${await response.text()}`);
}
return response.json() as Promise<BatchResponse>;
}
}
export { LinkzlyGamingClient, GameEvent, BatchResponse };
Sending Events
Session Start
Send a session_start event when the player begins a play session:
const sessionId = crypto.randomUUID();
const playerId = 'player-001'; // Your authenticated player ID
await gaming.sendEvents([{
event_id: crypto.randomUUID(),
event_type: 'session_start',
timestamp: new Date().toISOString(),
platform: 'web',
player_id: playerId,
session_id: sessionId,
data: {},
}]);
Tracking Game Events
// Level completed
await gaming.sendEvents([{
event_id: crypto.randomUUID(),
event_type: 'level_complete',
timestamp: new Date().toISOString(),
platform: 'web',
player_id: playerId,
session_id: sessionId,
data: {
level_id: 'level-7',
score: 24100,
duration_seconds: 134,
stars: 3,
},
}]);
// In-app purchase
await gaming.sendEvents([{
event_id: crypto.randomUUID(),
event_type: 'purchase',
timestamp: new Date().toISOString(),
platform: 'web',
player_id: playerId,
session_id: sessionId,
data: {
item_id: 'gem-pack-100',
amount: 0.99,
currency: 'USD',
store: 'stripe',
},
}]);
// Custom event
await gaming.sendEvents([{
event_id: crypto.randomUUID(),
event_type: 'custom',
timestamp: new Date().toISOString(),
platform: 'web',
player_id: playerId,
session_id: sessionId,
data: {
custom_event_name: 'boss_defeated',
boss_id: 'shadow-king',
difficulty: 'nightmare',
attempts: 5,
},
}]);
Batching Multiple Events
Send multiple events in a single request instead of one request per event. The ingestion endpoint accepts up to 1,000 events per batch (1 MB body limit, 10 KB per event):
// Collect events, then flush as a batch
const eventQueue = [];
function queueEvent(eventType, data = {}) {
eventQueue.push({
event_id: crypto.randomUUID(),
event_type: eventType,
timestamp: new Date().toISOString(),
platform: 'web',
player_id: playerId,
session_id: sessionId,
data,
});
}
async function flushEvents() {
if (eventQueue.length === 0) return;
const batch = eventQueue.splice(0, 1000); // Respect the 1,000 event limit
await gaming.sendEvents(batch);
}
// Usage
queueEvent('level_start', { level_id: 'level-8' });
queueEvent('currency_earned', { amount: 200, currency_type: 'gold' });
// Flush periodically or on session end
setInterval(flushEvents, 5000);
window.addEventListener('beforeunload', flushEvents);
HMAC Request Signing
If HMAC Signing is enabled for your game (the default for new games), every request must include a valid HMAC-SHA256 signature. Use the Web Crypto API — it is available in all modern browsers and in Node.js 18+:
async function computeSignature(signingSecret, timestamp, nonce, bodyString) {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(signingSecret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signingString = `${timestamp}.${nonce}.${bodyString}`;
const data = new TextEncoder().encode(signingString);
const signature = await crypto.subtle.sign('HMAC', key, data);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
async function sendEventsWithHMAC(events, signingSecret) {
const payload = {
batch_id: crypto.randomUUID(),
batch_timestamp: new Date().toISOString(),
events,
};
const bodyString = JSON.stringify(payload);
const timestamp = String(Date.now()); // Unix milliseconds
const nonce = crypto.randomUUID();
const signature = await computeSignature(signingSecret, timestamp, nonce, bodyString);
const response = await fetch(`${gaming.baseUrl}/api/v1/gaming/events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${gaming.sdkKey}`,
'X-Organization-ID': gaming.organizationId,
'X-Game-ID': gaming.gameId,
'X-Signature-256': signature,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
},
body: bodyString,
});
if (!response.ok) {
throw new Error(`Ingestion error ${response.status}: ${await response.text()}`);
}
return response.json();
}
How the signature is computed:
- Serialize the request body to a JSON string.
- Build the signing string:
"{timestamp}.{nonce}.{bodyString}" - Compute
HMAC-SHA256of the signing string using your signing secret as the key. - Hex-encode the resulting bytes.
- Send the result in
X-Signature-256, along withX-Timestamp(Unix milliseconds) andX-Nonce(UUID v4).
The replay window is fixed at 300 seconds. Requests timestamped more than 5 minutes from the server clock are rejected with 401 Unauthorized.
Security note: Never expose your signing secret in client-side JavaScript that is publicly accessible. If you are sending events from a browser, consider proxying requests through your backend, which holds the signing secret and computes the signature server-side.
Error Handling
Handle both network errors and HTTP error status codes:
async function safeSendEvents(events) {
try {
const result = await gaming.sendEvents(events);
if (result.events_dropped > 0) {
console.warn(
`${result.events_dropped} event(s) dropped. Trace ID: ${result.trace_id}`
);
}
return result;
} catch (error) {
if (error.message.includes('429')) {
// Rate limited — implement exponential backoff
console.warn('Rate limited by Linkzly ingestion API. Back off and retry.');
} else if (error.message.includes('401')) {
console.error('Authentication failed — check SDK key and HMAC signing configuration.');
} else if (error.message.includes('400')) {
console.error('Malformed event payload — check required fields.');
} else {
console.error('Linkzly send error:', error.message);
}
}
}
Rate limit: The ingestion endpoint allows 1,000 requests per minute with a burst allowance of 200. Responses include a Retry-After header when the limit is exceeded.
Node.js
The LinkzlyGamingClient class works identically in Node.js 18 and later, which includes native fetch. No additional libraries are required:
// Works in Node.js 18+ without any changes
const gaming = new LinkzlyGamingClient({
sdkKey: process.env.LINKZLY_SDK_KEY,
organizationId: process.env.LINKZLY_ORG_ID,
gameId: process.env.LINKZLY_GAME_ID,
});
await gaming.sendEvents([{
event_id: crypto.randomUUID(),
event_type: 'app_install',
timestamp: new Date().toISOString(),
platform: 'web',
player_id: 'player-001',
session_id: crypto.randomUUID(),
data: {},
}]);
For Node.js versions below 18, install the node-fetch package and import it, or use the https module directly.
Troubleshooting
CORS errors in the browser
The Linkzly ingestion endpoint allows cross-origin requests. If you see CORS errors, confirm the request URL is exactly https://gaming.linkzly.com/api/v1/gaming/events with no trailing slash or path typo.
Events dropped (events_dropped > 0)
Check that each event object includes all required fields: event_id, event_type, timestamp (ISO 8601), platform, player_id, and session_id. Events missing required fields are dropped. The trace_id in the response can be provided to Linkzly support for diagnosis.
beforeunload events not sending
Browsers may cancel in-flight fetch requests when the page unloads. Use navigator.sendBeacon() as a fallback for the final flush on page close:
window.addEventListener('beforeunload', () => {
const payload = JSON.stringify({
batch_id: crypto.randomUUID(),
batch_timestamp: new Date().toISOString(),
events: eventQueue,
});
navigator.sendBeacon(
'https://gaming.linkzly.com/api/v1/gaming/events',
new Blob([payload], { type: 'application/json' })
);
// Note: sendBeacon does not support custom headers.
// Use only when HMAC signing is disabled or when proxying through your backend.
});
Was this helpful?
Help us improve our documentation