# 155.io API Documentation > Complete API reference for integrating 155.io games into your platform. ## Overview 155.io provides a betting game platform (marble racing, duck racing, coin flip, etc.). Integration consists of two parts: 1. **155.io API** — Endpoints we provide for fetching games and generating player URLs 2. **Operator API** — Endpoints you implement for balance, bets, rollbacks, and wins You can also embed the game in an iframe and communicate with it over a `postMessage` API. ## Environments | Environment | Base URL | |-------------|----------| | Staging | `https://api.stagingmarbles.io` | | Production | `https://api.marbles.xyz` | ## Key Concepts ### Partner ID vs Operator ID - `partnerId` — You choose this. It identifies your website or casino brand (e.g., `"my-casino"`, `"my-casino-asia"`). You can use different partner IDs to distinguish between platforms or regions under the same operator. - `operatorId` — Provided by 155.io. Links to your operator configuration (liability limits, game settings, etc.). ### Currency Precision Amounts are sent as integers in the currency's minor units. **Precision varies by currency** — most currencies use 5 digits, but several cryptocurrencies use more. Always divide by `10^precision` for the human-readable amount. **Fiat, stablecoins (`USDT`/`USDC`/`DAI`) and special currencies use 5-digit precision** (the kilo variants `kIDR`/`kVND` use **8**): ``` $10.00 = 1000000 $1.00 = 100000 $0.01 = 1000 ``` **Cryptocurrencies use 6, 8, 9, or 10-digit precision** (per-currency, see the Cryptocurrencies table): ``` 1 BTC = 100000000 (8-digit precision, satoshi-style) 1 ETH = 100000000 (8-digit) 1 SOL = 1000000000 (9-digit) 1 ADA = 1000000 (6-digit) 1 XRP = 1000000 (6-digit) ``` Sending the wrong precision mis-renders amounts by orders of magnitude — e.g. sending `100000` for 1 BTC reads as `0.001 BTC` because BTC uses 8-digit precision. Use a 64-bit integer type — `long` (Java) / `int64` (Go/Protobuf) / `bigint` (JS) — not `int` (32-bit), which overflows at ~$21,474 for a 5-digit-precision currency. --- # Security Two distinct mechanisms protect the integration depending on who is calling whom — don't mix them up. ## Direction matrix | Direction | Endpoints | Authentication | What you do | |---|---|---|---| | **You → 155.io** (inbound) | `/game/game/url`, `/game/games`, `/game/round`, `/game/bets`, `/game/free-bets/rewards`, `/game/free-bets/rewards/cancel` | **IP whitelist.** Your server's outbound source IP must match the address(es) you registered during onboarding. | Make sure your outbound IP is the one we have on file. The `X-Marbles-Signature` header is **ignored** on these endpoints — you may send it, it will not be checked. | | **155.io → you** (outbound callbacks) | `/balance`, `/bet`, `/win`, `/rollback` | **RSA-SHA256 signature** in `X-Marbles-Signature`, signed with 155.io's private key. | **Verify** the signature (recommended) using 155.io's public key (shared at onboarding) and reject requests that fail — *or* strictly **IP-allowlist 155.io's egress IPs**. Either is accepted for go-live; verifying is the stronger option. | > Note: requests from you to 155.io are **not** signature-authenticated today — they are authenticated by source IP. Only 155.io's callbacks to your server are signed. ## Onboarding key exchange Submit during onboarding: 1. **Server IP address(es)** that will make outbound calls to 155.io — these are the IPs we whitelist for your inbound traffic. 2. **Your RSA public key** (PEM) — kept on file in case inbound signature verification is enabled later (see Roadmap). You receive: - **155.io's public key** (PEM) — use this to verify our outbound callbacks. - **155.io's outbound IP address(es)** — whitelist these on your firewall so our `/balance`, `/bet`, `/win`, `/rollback` calls reach your server. ## Generating your public key An RSA key pair has two halves. The **private key stays on your server and is never shared** — not with 155.io, not over email or chat. The **public key** is the half you send us; it can verify a signature but cannot create one, so it is safe to share. To generate one: ```bash openssl genrsa -out marbles-private.pem 2048 # keep this, never send it openssl rsa -in marbles-private.pem -pubout -out marbles-public.pem # send this one ``` The file you send must begin with `-----BEGIN PUBLIC KEY-----`. Use at least 2048 bits. Never send a private key. Files beginning `-----BEGIN PRIVATE KEY-----`, `-----BEGIN RSA PRIVATE KEY-----` or `-----BEGIN OPENSSH PRIVATE KEY-----` are the private half. If a private key has been shared, generate a fresh pair and send the new public key. Already have a key in another format? Converting only re-encodes it — it stays the same key (the `ssh-keygen -l -f key.pub` fingerprint is unchanged): | What you have | Starts with | Convert with | |---|---|---| | OpenSSH public key | `ssh-rsa AAAAB3Nza...` | `ssh-keygen -e -f key.pub -m PKCS8 > key.pem` | | PKCS#1 public key | `-----BEGIN RSA PUBLIC KEY-----` | `openssl rsa -RSAPublicKey_in -in key.pem -pubout -out public.pem` | | A private key | `-----BEGIN ... PRIVATE KEY-----` | `openssl rsa -in private.pem -pubout -out public.pem` | The most common mix-up is sending an SSH key: `ssh-keygen` produces keys for server logins, in a different encoding to the PEM format required here. ## Verifying our outbound signature Each callback we send to `/balance`, `/bet`, `/win`, `/rollback` includes `X-Marbles-Signature` — a base64-encoded RSA-SHA256 signature over the **raw request body bytes**. Compute the signature over the exact bytes you received (do not re-serialize, do not strip whitespace) and verify against 155.io's public key. ### Signature-Failure Response If verification fails, reject the request with a **non-2xx status (e.g. 401)** and no `status` field — a failed signature is an authentication failure, not a business outcome, and this is the one case where a non-200 is correct. There is **no** signature-specific `status` value (do not invent one such as INVALID_SIGNATURE — any unrecognised status is treated as UNKNOWN_ERROR, and on `/bet` that triggers an automatic `/rollback`). Every business outcome is still an HTTP 200 with the result in `status`. ### TypeScript Example ```typescript import { createSign, createVerify } from 'node:crypto' // Verify an incoming 155.io callback (/balance, /bet, /win, /rollback) function isValid(message: string, signature: string, publicKey: string): boolean { return createVerify('RSA-SHA256').update(message).verify(publicKey, signature, 'base64') } // Optional: sign your own responses (we don't currently verify, but it's forward-compatible) function sign(message: string, privateKey: string): string { return createSign('RSA-SHA256').update(message).sign(privateKey, 'base64') } ``` ### Python Example ```python from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding import base64 def is_valid(message: str, signature: str, public_key_pem: str) -> bool: public_key = serialization.load_pem_public_key(public_key_pem.encode()) try: public_key.verify(base64.b64decode(signature), message.encode(), padding.PKCS1v15(), hashes.SHA256()) return True except Exception: return False def sign(message: str, private_key_pem: str) -> str: private_key = serialization.load_pem_private_key(private_key_pem.encode(), password=None) return base64.b64encode(private_key.sign(message.encode(), padding.PKCS1v15(), hashes.SHA256())).decode() ``` ### PHP Example ```php function isValid(string $message, string $signature, string $publicKey): bool { return openssl_verify($message, base64_decode($signature), $publicKey, OPENSSL_ALGO_SHA256) === 1; } function sign(string $message, string $privateKey): string { openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256); return base64_encode($signature); } ``` ### Java Example ```java // Verify Signature verify = Signature.getInstance("SHA256withRSA"); verify.initVerify(publicKey); verify.update(message.getBytes()); boolean valid = verify.verify(Base64.getDecoder().decode(signatureStr)); // Sign (optional) Signature sig = Signature.getInstance("SHA256withRSA"); sig.initSign(privateKey); sig.update(message.getBytes()); String signature = Base64.getEncoder().encodeToString(sig.sign()); ``` ### C# Example ```csharp // Verify byte[] data = Encoding.UTF8.GetBytes(message); bool valid = publicKey.VerifyData(data, Convert.FromBase64String(signature), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // Sign (optional) byte[] signatureBytes = privateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); string signatureBase64 = Convert.ToBase64String(signatureBytes); ``` ## What we send and expect | Surface | `X-Marbles-Signature` header | Verified by recipient | |---|---|---| | Your request to 155.io (any inbound endpoint) | Optional. You may send one; we do not check it. | No | | 155.io response to you (any inbound endpoint) | Not sent. | N/A | | 155.io request to your `/balance`, `/bet`, `/win`, `/rollback` | Always sent, RSA-SHA256 over raw body bytes. | **Recommended** — verify it, or strictly IP-allowlist our egress IPs. | | Your response to our `/balance`, `/bet`, `/win`, `/rollback` | Optional. Forward-compatible but not currently checked. | No (today) | ## Timeouts The goal is **sub-second communication**, but each callback has a hard deadline: `/balance` 8s, `/bet` 8s, `/win` 30s, `/rollback` 30s. Exceeding the `/bet` deadline is treated as a rejection — an automatic `/rollback` is sent for that transaction, even if the wallet actually accepted the debit after the response was lost. ## Idempotency All money-moving callbacks must be processed idempotently — a retry must never move money twice. The dedupe key differs per callback: `/bet` and `/win` dedupe on `transactionId` (stable across retries; only `requestId` changes). `/rollback` dedupes on **`referenceTransactionId`** — the rollback's own `transactionId` is NOT stable between attempts and must never be the dedupe key (deduping rollbacks on it refunds the same stake once per attempt). ## Roadmap We may enable signature verification on inbound endpoints in the future. If you already sign your outbound requests today, no change will be needed when that happens. We will give advance notice before enforcing. --- # Provably Fair Every game on 155.io is provably fair: the outcome of each round is fixed by a secret seed committed before betting closes and revealed after the round settles. Anyone — player, operator, or auditor — can reproduce the exact outcome from the revealed seed with no trust in 155.io. The verifier is open source and runs entirely in the player's browser. There is no operator integration: nothing to implement, nothing to compute on your side. ## How it works - **Pre-committed hash chain.** Seeds form a chain where `seed[i] = SHA-256(seed[i+1] as its 64-char lowercase hex string, hashed as ASCII text)` — the hex TEXT is hashed, never the decoded bytes; the commitment uses the same encoding (`serverSeedHash = SHA-256(ASCII text of serverSeed)`). The chain's root hash is published up front. Round `j` uses `seed[j]`. A revealed seed hashes forward to the published root, but the root reveals nothing about future seeds. Seeds are consumed strictly in order, so a seed cannot be reordered or swapped in after the fact. Draws: `message = ":