SDK API client

The API client of the SDK signs every request, retries the calls that are safe to repeat and throws one error class per documented error code.

Create the API client

AggregatorClient takes the aggregator’s base URL, your API key and its secret. Every request it sends carries the four signature headers the authentication page describes, signed by the same module the aggregator verifies with. Its methods and types are generated from the OpenAPI document, so a field the API does not have fails to compile.

client.ts

import { AggregatorClient } from '@asteriks-aggregator/sdk/api'

/** One client per API key; it signs every request with the key’s secret. */
export function aggregatorClientOf(apiKey: string, secret: string): AggregatorClient {
  return new AggregatorClient({
    baseUrl: 'https://aggregator.asteriks.casino',
    apiKey,
    secret,
    timeoutMs: 10_000, // per attempt, the answer body included
    retry: { attempts: 3, baseDelayMs: 200, maxDelayMs: 5_000 },
  })
}

A GET is safe to repeat, so the API client retries it after a network failure, a timeout or an undocumented 5xx, up to retry.attempts attempts in all, with a random backoff between them. A POST is sent once: a session launch is never sent twice. The options fetch, now, nonce, sleep and random replace the global fetch, the clock and the random sources, for example in your tests.

Launch a game

createSession sends the body of Create a session and returns the game_url to open. In real mode it creates a session; in demo mode the game opens without one and session_id is null. The sessions page explains both modes.

A BSW sportsbook launch answers frame, the data its frame boots with, in place of game_url, so narrow the answer with 'game_url' in session before you read game_url; Create a session shows both answers. An ElectroBets sportsbook launch answers a game_url to open in an iframe; Sessions and launch lists the events its page posts to yours.

launch.ts

import { type AggregatorClient, LaunchNotSupportedError, type SessionCreated } from '@asteriks-aggregator/sdk/api'

/** The launch of a game in real mode, or undefined when the game cannot be played in the player’s currency. */
export async function launch(
  client: AggregatorClient,
  playerId: string,
  gameId: string,
): Promise<SessionCreated | undefined> {
  try {
    const session = await client.createSession({
      player_id: playerId,
      currency: 'EUR',
      game_id: gameId,
      language: 'en',
      country: 'TR',
      device: 'desktop',
      return_url: 'https://casino.example.com/lobby',
      mode: 'real',
    })
    if (!('game_url' in session)) throw new Error('a sportsbook answers a frame, not a game_url')
    return session
  } catch (error) {
    if (error instanceof LaunchNotSupportedError && error.code === 'CURRENCY_NOT_SUPPORTED') return undefined
    throw error
  }
}

Boot a sportsbook frame

openSportsbook launches a sportsbook in real mode. When the answer has frame, bootFrame loads the two scripts in their order and boots the BSW frame in the container container_id. Otherwise the answer is an ElectroBets launch, and the function returns the game_url to open in an iframe. The sportsbook page explains each step.

sportsbook.ts

import type { AggregatorClient, SportsbookFrame } from '@asteriks-aggregator/sdk/api'

type BswGlobals = {
  SportFrame?: { frame: (pairs: [string, unknown][]) => void }
  Bootstrapper?: { bootIframe: (options: Record<string, unknown>, mode: { name: 'Mobile' }) => void }
}

function loadScript(src: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src
    script.onload = () => resolve()
    script.onerror = () => reject(new Error(`could not load ${src}`))
    document.head.append(script)
  })
}

export async function bootFrame(frame: SportsbookFrame): Promise<void> {
  if (document.getElementById(frame.container_id) === null) throw new Error(`add the container ${frame.container_id}`)
  // The integration loader comes first, then the bootstrapper.
  for (const src of frame.scripts) await loadScript(src)
  const bsw = window as Window & BswGlobals
  if (frame.device === 'desktop') {
    if (bsw.SportFrame === undefined) throw new Error('SportFrame is missing')
    bsw.SportFrame.frame([
      ['server', frame.server],
      ['token', frame.token],
      ['parent', [location.host]],
      ['currentPage', 'Home'],
      ['language', frame.language],
      ['sportPartner', frame.sport_partner],
    ])
    return
  }
  if (bsw.Bootstrapper === undefined) throw new Error('Bootstrapper is missing')
  bsw.Bootstrapper.bootIframe(
    {
      server: frame.server,
      containerId: frame.container_id,
      token: frame.token,
      parent: [location.host],
      defaultLanguage: frame.language,
      sportPartner: frame.sport_partner,
    },
    { name: 'Mobile' },
  )
}

export async function openSportsbook(
  client: AggregatorClient,
  playerId: string,
  gameId: string,
  device: 'desktop' | 'mobile',
): Promise<string | undefined> {
  const session = await client.createSession({
    player_id: playerId,
    currency: 'EUR',
    game_id: gameId,
    language: 'en',
    country: 'TR',
    device,
    return_url: 'https://casino.example.com/lobby',
    mode: 'real',
  })
  if ('frame' in session) {
    await bootFrame(session.frame)
    return undefined
  }
  // ElectroBets: open this game_url in an iframe.
  return session.game_url
}

Read the catalogue page by page

Every list operation has two methods: listGames returns one page with its next_cursor, and listGamesAll follows the cursors and yields every item of every page. The pagination page describes the cursors, and List games the filters.

catalogue.ts

import type { AggregatorClient, GameSummary } from '@asteriks-aggregator/sdk/api'

/** Every mobile game of one provider, across all pages. */
export async function mobileGames(client: AggregatorClient): Promise<GameSummary[]> {
  const games: GameSummary[] = []
  for await (const game of client.listGamesAll({ provider: 'drakon', device: 'mobile', limit: 100 }))
    games.push(game)
  return games
}

The same pair exists for rounds and transactions: listRounds and listRoundsAll, listTransactions and listTransactionsAll, filtered by player, game and time range. getRound returns one round with its transactions, and getReconciliationReport the report of one UTC day, or getReconciliationReportCsv the same report as CSV.

history.ts

import type { AggregatorClient, RoundSummary } from '@asteriks-aggregator/sdk/api'

/** The rounds one player opened on one UTC day, and that day’s reconciliation report as CSV. */
export async function dayOf(
  client: AggregatorClient,
  playerId: string,
  day: string,
): Promise<{ rounds: RoundSummary[]; report: string }> {
  const rounds: RoundSummary[] = []
  const to = new Date(Date.parse(`${day}T00:00:00Z`) + 86_400_000).toISOString()
  for await (const round of client.listRoundsAll({ playerId, from: `${day}T00:00:00Z`, to })) rounds.push(round)
  return { rounds, report: await client.getReconciliationReportCsv({ date: day }) }
}

Handle errors

Every error the API client throws is an AggregatorError with the operationId of the call. Its subclasses tell the cases apart:

  • AggregatorApiError: the aggregator answered with its error body; status and code are the answer’s. Each documented code has its own class: MalformedRequestError, UnauthorizedError, NotFoundError, LaunchNotSupportedError, CampaignConflictError, CampaignNotSupportedError and InternalError. The errors page lists the codes.
  • AggregatorTransportError: no answer arrived, after attempts attempts. Its cause keeps only a safe name and code, never the request’s headers.
  • AggregatorResponseError: the answer matches nothing the contract documents for the operation.

errors.ts

import {
  AggregatorApiError,
  type AggregatorClient,
  AggregatorTransportError,
  type GameSummary,
  NotFoundError,
} from '@asteriks-aggregator/sdk/api'

/** One game, or undefined when the id is unknown; a failure to reach the aggregator is reported and thrown again. */
export async function gameOf(client: AggregatorClient, gameId: string): Promise<GameSummary | undefined> {
  try {
    return await client.getGame({ gameId })
  } catch (error) {
    if (error instanceof NotFoundError) return undefined
    if (error instanceof AggregatorTransportError) console.warn(`${error.operationId}: no answer after ${error.attempts} attempts`)
    else if (error instanceof AggregatorApiError) console.warn(`${error.operationId}: ${error.status} ${error.code}`)
    throw error
  }
}

Was this page helpful?