---
title: "Install Challenge.js"
description: "The two script tags, what the instant-render stub does, init({ program }) and the options worth setting, overlay and inline placement, SPA re-init versus explicit destroy(), and apiBase for local development."
url: https://www.withchallenge.com/docs/install
canonical_markdown: https://www.withchallenge.com/docs/install.md
docs_index: https://www.withchallenge.com/llms.txt
api_version: v1
lastUpdated: 2026-09-22T16:56:18.000Z
---

# Install Challenge.js

Two script tags, one call, and the island is on your page.

**Runs in the browser**

Challenge.js is distributed as script tags only. There is no npm package, for the browser or for the server, and there is nothing to bundle. The whole browser half of the integration is the two tags below plus one `init()` call.

## The two script tags

Put both in the page that runs your game, in this order.

```html
<script src="https://api.withchallenge.com/widget/dist/island-stub.js"></script>
<script async src="https://api.withchallenge.com/widget/dist/island.js"></script>
```

The first is synchronous and tiny. The second is the rest of the SDK and loads out of the way.

## What the stub does

The stub is on the critical path of someone else's game, so it does exactly three things and nothing else.

1. **It defines `<challenge-island>`** as a custom element and paints the dormant pill into its shadow DOM immediately. Your page never shows a hole where the money surface is about to be.
2. **It queues `init()`.** A call that lands before `island.js` has arrived is pushed onto `Challenge._queue` and replayed the moment the full bundle takes over, so `Challenge.init(...)` is safe to run from the first line of your boot code.
3. **It holds one early tap.** If a player taps the pill before the bundle is live, the stub dims it and remembers a single tap, which is replayed once there is something to open. One tap, never a list: replaying a burst would run the arm flow several times over.

The stub makes no network request beyond its font, and it cannot know whether the player has anything armed, so it always paints the dormant costume and lets the bundle correct it.

If you want the pill to land in a specific corner before `init()` has run, name it on the stub's own tag:

```html
<script src="https://api.withchallenge.com/widget/dist/island-stub.js" data-anchor="top-center"></script>
```

The anchor has to be spelled out. An empty `data-anchor` mounts nothing.

## Initialise

Once, wherever your game boots:

```javascript
Challenge.init({ program: '<<YOUR_PROGRAM_ID>>' })
```

`program` is the only required option. It is your program ID from the [Developer Portal](https://app.withchallenge.com), it is public, and it belongs in the browser. Your `sk_test_` and `sk_live_` keys do not: see [Programs and keys](https://www.withchallenge.com/docs/programs-and-keys.md).

`init()` returns the `Challenge` object, so it chains. A call with no `program`, or with a non-string one, logs an error and mounts nothing.

Until your program has a live Challenge, the island draws a small dormant pill reading **Coming soon**. It offers nothing and charges nothing, but your players do see it, so decide deliberately whether to ship this line before a live Challenge exists.

## The options you will actually set

The full option table is on the [Challenge.js reference](https://www.withchallenge.com/docs/challenge-js.md). These are the ones that come up in practice.

| Option | Type | Default | What it does |
|---|---|---|---|
| `program` | string | required | Your program ID |
| `anchor` | string | `'bottom-right'` | An overlay corner, or a CSS selector for an inline mount |
| `size` | `'regular'` or `'compact'` | `'regular'` | How large the pill draws |
| `theme` | `'light'` or `'dark'` | `'light'` | The palette for every island surface |
| `draggable` | boolean | `false` | Lets the player move the island, and remembers where they put it |
| `challenge` | string | `null` | Pin the island to one Challenge id instead of letting it pick from your catalog |
| `apiBase` | string | script origin | Local development only |

### Placement

Six names mount the island as a fixed overlay: `bottom-right`, `bottom-left`, `bottom-center`, `top-right`, `top-left`, `top-center`. Safe-area insets are handled, so the island clears a notch and a home indicator without you doing anything.

Anything else is read as a CSS selector, and the island renders inline inside your own layout:

```javascript
Challenge.init({
  program: '<<YOUR_PROGRAM_ID>>',
  anchor: '#challenge-slot',
})
```

A selector that matches nothing falls back to the default overlay corner and says so loudly in the console. The island cannot simply not appear: while a player is armed it is the only thing on the page telling them money is at stake.

Which edge is pinned is which way the island grows. A top anchor expands downward, a bottom anchor expands upward, so an expanded island at any of the five corners is fully on screen by construction.

### Theme

One choice reaches every surface: the dormant pill, the armed costumes, the arming card and the account panel. An unrecognised value renders light rather than erroring, because a theme is not worth refusing to render over.

`dormantTheme` is a deprecated alias of `theme`. It still works and logs one console warning per page; `theme` wins when both are given.

### Pinning one Challenge

The option key is literally `challenge`, not `challenge`:

```javascript
Challenge.init({
  program: '<<YOUR_PROGRAM_ID>>',
  challenge: '<<YOUR_CHALLENGE_ID>>',
})
```

Pinning is filtered against live Challenges only. If the id you named is not live, the island warns, falls back to the first Challenge that is, and keeps working. A partner with one Challenge in their game wants this; a partner with a catalog should leave it off and let the island offer the whole list.

### Options that are gone

Passing any of `apiKey`, `gameId`, `entryFee`, `mode`, `matchmaking`, `settlement`, `playerRating`, `position`, `variant`, `dashboardUrl`, `challengeDomain`, `ssoEnabled` or `ssoTimeout` logs a warning and is ignored. They belong to the retired versus model. `apiKey` in particular must never appear in browser code at all.

## Single-page apps

`init()` is safe to call on every route change, and that is the intended use.

- **`init()` never calls `destroy()`.** That is a house rule, not an implementation detail. An implicit teardown inside `init` is how a re-render silently drops a player's state.
- The `<challenge-island>` element is **reused** when `anchor` and `draggable` have not changed, so navigating inside your app does not blink the island out and back.
- The player's **armed session survives** a re-init, and so does a **paid attempt that is already running**, because a route change is not the end of a round.
- **`destroy()` is explicit and total.** It tears down the element, every listener and timer, the state machine, the settlement socket and its poll, any queued `record()` calls, and the remembered session. Call it when the game leaves the page for good, not on every navigation.

A complete React mount and unmount. Note that `destroy()` lives on the component that owns the game for the whole session, not on each route:

```jsx
import { useEffect } from 'react'

export function ChallengeProvider({ programId, children }) {
  useEffect(() => {
    // Safe on every route change. The element and the armed session are reused.
    window.Challenge.init({
      program: programId,
      anchor: 'bottom-right',
      theme: 'dark',
    })

    return () => {
      // Only on real unmount. destroy() also forgets the player's armed
      // session, so calling it between routes would disarm them silently.
      window.Challenge.destroy()
    }
  }, [programId])

  return children
}
```

With the two script tags in `index.html`:

```html
<!doctype html>
<html>
  <head>
    <script src="https://api.withchallenge.com/widget/dist/island-stub.js"></script>
    <script async src="https://api.withchallenge.com/widget/dist/island.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
```

If you write `<challenge-island>` into your own markup rather than letting the overlay create one, an element that connects after `init()` has run is adopted and painted rather than replaced.

## Local and self-hosted sandboxes

The API base is detected from the origin the script was loaded from, so a production page needs no configuration. Point it somewhere else only when the sandbox you are integrating against is a Challenge backend running somewhere other than `api.withchallenge.com`, such as a local one on port 4837.

Both script tags come from that same origin, and `apiBase` names it as well:

```html
<script src="http://localhost:4837/widget/dist/island-stub.js"></script>
<script async src="http://localhost:4837/widget/dist/island.js"></script>
```

```javascript
Challenge.init({
  program: '<<YOUR_PROGRAM_ID>>',
  apiBase: 'http://localhost:4837',
})
```

Load the scripts and set `apiBase` together. A page that loads the bundle from one origin and talks to another is the one configuration the island cannot detect its way out of.

Everything else on that backend is at the same base, including these docs: a self-hosted or local backend serves every page at `{your API base}/api/docs/<slug>.md`, for example `http://localhost:4837/api/docs/install.md`. The agent index sits beside them under the same `/api/docs` path, named `llms.txt`.

Read `Challenge.version` in the console to confirm which bundle a page is running.

## Next

- [Record rounds](https://www.withchallenge.com/docs/record-rounds.md) is the next line to add, and the one that ships before anything chargeable exists.
- [Start a paid attempt](https://www.withchallenge.com/docs/start-attempt.md) covers `startAttempt()` and branching on its error codes.
- [Challenge.js reference](https://www.withchallenge.com/docs/challenge-js.md) has every option, method and error code, plus the catalog elements.

---

Source: https://www.withchallenge.com/docs/install.md
Docs index for AI agents: https://www.withchallenge.com/llms.txt
