Linkzly

Web / JavaScript SDK Setup Guide

---

8 min read

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:

  1. Serialize the request body to a JSON string.
  2. Build the signing string: "{timestamp}.{nonce}.{bodyString}"
  3. Compute HMAC-SHA256 of the signing string using your signing secret as the key.
  4. Hex-encode the resulting bytes.
  5. Send the result in X-Signature-256, along with X-Timestamp (Unix milliseconds) and X-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