Install
There is one SDK, loaded from one script tag. There's no npm package to install or keep updated: bundlers just load the same tag. What changes per stack is where you find scene, camera and renderer.
Script tag
Every install starts here. Use the tag from your game's page in the dashboard; it already has your data-pub.
<script async src="https://bonusround.io/v1/br.js" data-pub="pub_XXXXXXXX"></script>
- Where: the
<head>of the page that runs the game. If the game runs in an iframe (itch.io, Poki, CrazyGames, your own embed), it goes in the page inside the iframe. - What it does: defines
window.BonusRound, pings Bonus Round once the page has loaded (that's how your game gets detected), and imports the rest of the SDK only when it's needed. - Optional config: set
window.bonusroundConfigbefore the tag.{ muted: true }starts the round's music and sound effects muted;{ pub }works in place ofdata-pub;{ consent: false }(ordata-storage="none"on the tag) keeps the anonymous player id in memory only. See Privacy.
Calling the SDK before it has loaded
The tag is async, so your game code may run first. Push a function onto the bonusround queue and it runs as soon as the SDK is there (or right away if it already is):
(window.bonusround = window.bonusround || []).push((BR) => BR.attach({ THREE, scene, camera, renderer }));
Use the queue for attach(). For break(), which runs later in the game, await window.BonusRound?.break('intermission') is enough.
Vanilla three.js (import map or CDN)
A plain HTML page with an import map is the simplest setup. Add the tag to index.html and the attach line to your main module, after the renderer, scene and camera are created.
<head>
<script type="importmap">{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.186.0/build/three.module.js" } }</script>
<script async src="https://bonusround.io/v1/br.js" data-pub="pub_XXXXXXXX"></script>
</head>
<body>
<script type="module" src="./main.js"></script>
</body>
// main.js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 500);
(window.bonusround = window.bonusround || []).push((BR) => BR.attach({ THREE, scene, camera, renderer }));
Using the old global build (<script src="three.min.js">)? Pass the global: BR.attach({ THREE: window.THREE, scene, camera, renderer }).
npm and bundlers (Vite, webpack, Parcel, Next.js)
Keep using three from npm as you do now. Load the SDK with the script tag in your HTML template, and call it from your bundled code.
Vite's HTML entry is index.html in the project root. Add the tag to its <head>, then attach in the module that creates the renderer (often src/main.js or src/main.ts).
<!-- index.html -->
<head>
<script async src="https://bonusround.io/v1/br.js" data-pub="pub_XXXXXXXX"></script>
</head>
// src/main.ts
import * as THREE from 'three';
// ...create renderer, scene, camera...
(window.bonusround = window.bonusround || []).push((BR) => BR.attach({ THREE, scene, camera, renderer }));
With html-webpack-plugin, add the tag to your template (often public/index.html or src/index.html). Create React App uses public/index.html.
<!-- public/index.html -->
<head>
<script async src="https://bonusround.io/v1/br.js" data-pub="pub_XXXXXXXX"></script>
</head>
import * as THREE from 'three';
// ...create renderer, scene, camera...
(window.bonusround = window.bonusround || []).push((BR) => BR.attach({ THREE, scene, camera, renderer }));
If your build owns the HTML (some Next.js or game-engine setups), inject the tag from code. This is the same tag, created at runtime:
import * as THREE from 'three';
const s = document.createElement('script');
s.src = 'https://bonusround.io/v1/br.js';
s.async = true;
s.dataset.pub = 'pub_XXXXXXXX';
document.head.append(s);
// The queue works even though the script was just added:
(window.bonusround = window.bonusround || []).push((BR) => BR.attach({ THREE, scene, camera, renderer }));
TypeScript
Add a small declaration so window.BonusRound and the queue type-check:
// src/bonusround.d.ts
type BonusRoundResult = { filled: boolean; completed: boolean; reason?: string; score?: number };
interface BonusRoundSDK {
version: string;
attach(opts: { THREE: unknown; scene: unknown; camera: unknown; renderer: unknown; worldRoot?: unknown; host?: unknown }):
Promise<{ mode: 'overlay' | 'native-local' | 'native-net'; playerId?: string; already?: boolean }>;
break(trigger?: 'intermission' | 'test'): Promise<BonusRoundResult>;
rewarded(opts: { onReward: (r: BonusRoundResult) => void; label?: string; button?: boolean }):
Promise<(BonusRoundResult & { rewarded?: boolean }) | { shown: true; hide(): void; start(): Promise<BonusRoundResult> }>;
safe(v?: boolean | null): BonusRoundSDK;
placeAmbient(hint: { position: [number, number, number]; rotationY?: number } | null): BonusRoundSDK;
on(type: string, cb: (data: any) => void): BonusRoundSDK;
off(type: string, cb: (data: any) => void): BonusRoundSDK;
config(o: { muted?: boolean; test?: boolean; storage?: false | null }): BonusRoundSDK;
consent(v: boolean): BonusRoundSDK;
debug(): Promise<unknown>;
ready(): Promise<BonusRoundSDK>;
}
declare global {
interface Window {
BonusRound?: BonusRoundSDK;
bonusround?: Array<(br: BonusRoundSDK) => void> | { push: (fn: (br: BonusRoundSDK) => void) => number };
bonusroundConfig?: { pub?: string; muted?: boolean; consent?: boolean; storage?: false };
}
}
export {};
React Three Fiber
In R3F you never call new WebGLRenderer() yourself. useThree() gives you all three: scene, camera and gl (the WebGLRenderer). Add this component inside your <Canvas>, and the script tag to your index.html as above.
// BonusRoundAttach.jsx
import { useEffect } from 'react';
import { useThree } from '@react-three/fiber';
import * as THREE from 'three';
export function BonusRoundAttach() {
const { scene, camera, gl } = useThree();
useEffect(() => {
(window.bonusround = window.bonusround || []).push((BR) => BR.attach({ THREE, scene, camera, renderer: gl }));
}, [scene, camera, gl]);
return null;
}
// App.jsx
<Canvas>
<BonusRoundAttach />
<MyGame />
</Canvas>
Call break() from your game state (a Zustand store, a reducer, a useEffect on phase === 'roundOver'): it's a plain promise and doesn't need to be inside the Canvas.
Switching cameras at runtime (a cutscene camera, say)? The SDK keeps the camera you attached. Attach the camera players actually see during play.
Let the CLI do it
npx bonusround init finds your three.js setup (package.json, import map or CDN), finds new THREE.WebGLRenderer(, your scene and camera, and shows you a diff that adds the script tag and the attach line. Nothing is written until you say yes. It also marks a likely natural break with a TODO(bonusround) comment.
npx bonusround login # paste an API key from /app/developers
npx bonusround init # shows the diff, asks before writing
npx bonusround status --wait 60
Requirements
- three.js with
WebGLRenderer. The SDK uses theTHREEyou pass, so it renders with your version and reports it to the dashboard. - Models in Bonus Rounds are plain glTF/GLB (no Draco, meshopt or KTX2 decoders needed on your side).
- A page served over
https://in production.localhostalways works for development.