# Apogee Quickstart — 5 minutes to a running game

You already received your **merchant ID** and **API keys** from Apogee onboarding. This doc gets you from those credentials to a working Skyward launch for one test player in about 5 minutes.

If you don't have credentials yet: **partners@apogeetech.net**.

## Live URLs

| | URL |
|---|---|
| 🎮 Game | **https://apogeetech.net** (direct: https://apogee-web-634190752875.europe-west1.run.app) |
| 🔧 Sandbox API | `https://sandbox-api.apogeetech.net` |
| 🔧 Live API | `https://api.apogeetech.net` |

You do **not** visit the admin URL — that's Apogee staff-only.

---

## 1. Set your env vars

```bash
export APOGEE_KEY="pk_test_apogee_…"     # from your onboarding email
export APOGEE_SECRET="sk_test_apogee_…"  # store in a secrets manager, not source
export APOGEE_BASE="https://sandbox-api.apogeetech.net"
```

## 2. Create a session (backend)

A minimal signed request. Full cURL with a real signature is generated live in the Apogee signature tester — ask your partner manager for a one-off link.

Node.js:
```js
import crypto from "node:crypto";

const body = {
  gameId:    "skyward",
  playerId:  "p_test_001",
  currency:  "KES",            // ← match the player's wallet currency
  balance:   100000,           // ← 1000.00 KES expressed in cents (minor units)
  returnUrl: "https://yourcasino.example/lobby",
  language:  "en",
};

const bodyStr = JSON.stringify(body);
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(bodyStr).digest("hex");
const canonical = ["POST", "/v1/sessions", ts, nonce, bodyHash].join("\n");
const sig = crypto.createHmac("sha256", process.env.APOGEE_SECRET).update(canonical).digest("hex");

const res = await fetch(process.env.APOGEE_BASE + "/v1/sessions", {
  method: "POST",
  headers: {
    "X-Apogee-Key":       process.env.APOGEE_KEY,
    "X-Apogee-Timestamp": ts,
    "X-Apogee-Nonce":     nonce,
    "X-Apogee-Signature": sig,
    "Content-Type":       "application/json",
  },
  body: bodyStr,
});

const { launchUrl } = await res.json();
// Serve launchUrl to the player's browser
```

**Critical:** `currency` is the player's wallet currency. If your player holds KES, pass `"KES"` and send `balance` in KES cents. The game will display everything in KES and your `/wallet/debit` will be called with KES amounts. No conversion happens.

**Note on bet lifecycle.** Skyward uses an opt-in re-entry model: after a player cashes out (or loses), they are **not** auto-queued for the next round. Their bet button switches to `JOIN NEXT ROUND` and they must tap it to participate again. This means `/wallet/debit` fires only when the player has consciously chosen to bet — no surprise round-after-round debits. See [INTEGRATION.md §5](./INTEGRATION.md#gameplay-note-opt-in-re-entry-after-cashout).

## 3. Response

```json
{
  "sessionToken": "sess_01HXYZ…",
  "launchUrl":    "https://apogeetech.net/contrail.html?merchant=mrc_edilbet&player=p_test_001&currency=KES&lang=en&sess=sess_01HXYZ…",
  "expiresAt":    "2026-04-11T12:30:00Z"
}
```

**Use the `launchUrl` exactly as returned.** Apogee-api is the single
source of truth for per-game launch paths — each game has its own HTML
bundle (Skyward and Thermal share `play.html`; Contrail lives at
`contrail.html`; Apogee Hot 5 at `apogeehot5.html`; future games may
have their own paths). **Do not hardcode** `/play.html` into your
integration or you'll silently serve the wrong game when we add new
titles.

If you prefer a single bare-domain launch URL that auto-routes to the
right bundle, this also works:

```
https://apogeetech.net/?gameId=<gameId>&merchant=<mrc>&player=<pid>&currency=<ccy>&sess=<token>
```

The landing page and `play.html` both have client-side forwarders that
delegate to `apogee-api`'s `/v1/launch?...&redirect=1` endpoint, which
302s to the correct HTML. This means `apogeetech.net/?gameId=contrail`,
`apogeetech.net/play.html?gameId=contrail`, and
`apogeetech.net/contrail.html` all resolve to the same live Contrail
session — but the **canonical form for new integrations is the first
one** (the bare domain with `?gameId=`). See
[INTEGRATION-PITFALLS.md §31](./INTEGRATION-PITFALLS.md#31-wrong-launch-path-for-multi-bundle-games--fixed-2026-04-12).

## 4. Embed the iframe

```html
<iframe
  src="{{launchUrl}}"
  width="100%" height="720"
  allow="autoplay; fullscreen"
  referrerpolicy="strict-origin-when-cross-origin">
</iframe>
```

Tell your CSP to allow it:
```
frame-src 'self' https://apogeetech.net;
connect-src 'self' https://api.apogeetech.net;
```

## 5. Host four wallet endpoints

Apogee calls these on every bet and every payout. Skeleton (Node / Express):

```js
// All four endpoints live under your chosen base URL.
// Apogee calls them with a signed request (same HMAC scheme).
// You told Apogee this base URL during onboarding.

app.post("/wallet/balance", verifyApogeeSignature, async (req, res) => {
  const { playerId } = req.body;
  const player = await db.players.findOne({ id: playerId });
  res.json({ balance: player.balance, currency: player.currency });
});

app.post("/wallet/debit", verifyApogeeSignature, async (req, res) => {
  const { playerId, amount, txId } = req.body;
  // Idempotency — if we've seen this txId, return the original response
  const existing = await db.txs.findOne({ txId });
  if (existing) return res.json(existing.response);

  const player = await db.players.findOne({ id: playerId });
  if (player.balance < amount) return res.status(402).json({ code: "insufficient_funds" });

  player.balance -= amount;
  await db.players.save(player);
  const response = { balance: player.balance, currency: player.currency, txId };
  await db.txs.insert({ txId, response });
  res.json(response);
});

app.post("/wallet/credit", verifyApogeeSignature, async (req, res) => {
  // Same shape — amount is the payout, idempotent on txId
  const { playerId, amount, txId } = req.body;
  const existing = await db.txs.findOne({ txId });
  if (existing) return res.json(existing.response);

  const player = await db.players.findOne({ id: playerId });
  player.balance += amount;
  await db.players.save(player);
  const response = { balance: player.balance, currency: player.currency, txId };
  await db.txs.insert({ txId, response });
  res.json(response);
});

app.post("/wallet/rollback", verifyApogeeSignature, async (req, res) => {
  // If we've never seen this txId, no-op: return current balance
  const { playerId, txId } = req.body;
  const tx = await db.txs.findOne({ txId });
  const player = await db.players.findOne({ id: playerId });
  if (!tx) return res.json({ balance: player.balance, currency: player.currency, txId });
  // Otherwise reverse the transaction
  await reverseTx(tx, player);
  res.json({ balance: player.balance, currency: player.currency, txId });
});
```

Full spec and error codes: [API.md](./API.md).

## 6. Play

Open your page. Apogee will call your wallet on every bet and payout. You're done.

---

## Currency examples

Apogee supports many currencies — the `currency` field on your session request is the single source of truth for the session.

| Player wallet | `currency` | `balance` (for 1,000.00 in the player's wallet) |
|---|---|---|
| Euro                    | `"EUR"` | `100000` (100000 cents) |
| Kenyan Shilling         | `"KES"` | `100000` (100000 KES cents) |
| Ethiopian Birr          | `"ETB"` | `100000` |
| Zambian Kwacha          | `"ZMW"` | `100000` |
| South Sudanese Pound    | `"SSP"` | `100000` |
| Somali Shilling         | `"SOS"` | `100000` |
| Somaliland Shilling     | `"SLS"` | `1000`   (zero decimals — minor unit = whole) |
| Djiboutian Franc        | `"DJF"` | `1000`   (zero decimals) |
| Moroccan Dirham         | `"MAD"` | `100000` |
| Japanese Yen            | `"JPY"` | `1000`   (zero decimals) |
| Bitcoin                 | `"BTC"` | `100000000` (1.0 BTC = 100,000,000 satoshis) |
| Tether                  | `"USDT"`| `1000000` (6 decimals) |

Full list in [INTEGRATION.md §5](./INTEGRATION.md#5-launching-a-game). Need a currency that isn't listed? Email partners@apogeetech.net — it's a config flip on our side, not a code change.

## Next steps

- **More games:** just pass a different `gameId` to `/v1/sessions`. See [GAMES.md](./GAMES.md).
- **Analytics / leaderboards:** hook up webhooks, [WEBHOOKS.md](./WEBHOOKS.md).
- **Go live:** swap your test keys for live keys and point at `https://api.apogeetech.net`.
