155API

Security

How 155.io authenticates traffic in both directions


Two distinct mechanisms protect the integration depending on who is calling whom. Read both — they're easy to mix up.

Quick answer if you're here from a support thread

Requests from you to 155.io (e.g. /game/url, /game/games) are authenticated by IP whitelist. We do not verify the X-Marbles-Signature header on inbound requests — you may send it, it will be ignored. Requests from 155.io to your server (/balance, /bet, /win, /rollback) are signed with X-Marbles-Signature, and you must verify them.

Direction matrix

DirectionEndpointsAuthenticationWhat you do
You → 155.io (inbound)/game/url, /game/games, /game/grant-freebet, /game/round, /game/bets, /free-bets/rewardsIP whitelist. Source IP must match the address(es) you registered during onboarding.Make sure your outbound IP is the one we have on file. The signature header is ignored on these endpoints.
155.io → you (outbound callbacks)/balance, /bet, /win, /rollbackRSA-SHA256 signature in X-Marbles-Signature, signed with 155.io's private key.Verify the signature using the 155.io public key we share at onboarding. Reject requests where verification fails.

Onboarding

Submit during onboarding:

  1. Server IP address(es) that will make outbound calls to 155.io — these are the IPs we whitelist for inbound traffic.
  2. Your RSA public key in PEM format — we keep this on file for the case where we re-enable inbound verification (see Roadmap).

You receive:

  • 155.io's public key in PEM format — 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.

Where to send keys

Contact the tech team to exchange public keys before starting your integration.

Verifying our outbound signature

Each request we send to your /balance, /bet, /win, /rollback endpoints includes X-Marbles-Signature — a BASE64-encoded RSA-SHA256 signature of the raw request body bytes. Compute the signature over the exact body bytes you received (do not re-serialize, do not strip whitespace) and verify against our public key.

If verification fails, respond with the body shown below. We log the rejection and treat it like any other delivery failure (retry or rollback flow, depending on the endpoint).

HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "INVALID_SIGNATURE",
  "requestId": "<echo back the requestId from the request>",
  "clientPlayerId": "<echo back the clientPlayerId from the request>"
}

Always HTTP 200

Return HTTP 200 even when rejecting on signature failure. We treat non-200 HTTP responses as transport-level failures, which can trigger different recovery paths than a business-logic failure does.

Implementation Example

These samples implement both directions: sign() is what 155.io does on outbound (shown for reference if you want to mirror the format on your responses), and isValid() is the one you need — it verifies our X-Marbles-Signature header on incoming requests.

import fs from 'fs'
import path from 'path'
import { createSign, createVerify } from 'node:crypto'

export const CryptoService = (
  digestType: string,
  privateKey: string,
  publicKey: string
) => {
  const sign = (message: string) => {
    return createSign(digestType)
      .update(message)
      .sign(privateKey, 'base64')
  }

  const isValid = (message: string, signature: string) => {
    return createVerify(digestType)
      .update(message)
      .verify(publicKey, signature, 'base64')
  }

  return { sign, isValid }
}

function readPem(filename: string) {
  return fs
    .readFileSync(path.resolve('path/to/your/pem/' + filename))
    .toString('ascii')
}

// Usage
const publicKey = readPem('marbles_pub.pem')          // 155.io's public key
const privateKey = readPem('your_priv.pem')           // your private key (optional, for signing responses)
const cryptoService = CryptoService('RSA-SHA256', privateKey, publicKey)

// Verify an incoming 155.io callback (/balance, /bet, /win, /rollback)
const signature = request.headers['x-marbles-signature']
const isValid = cryptoService.isValid(rawRequestBody, signature)
if (!isValid) {
  return response.status(200).json({ status: 'INVALID_SIGNATURE', requestId, clientPlayerId })
}

// Optional: sign your response body (we don't currently verify, but signing is forward-compatible)
const responseSignature = cryptoService.sign(JSON.stringify(responseBody))
response.setHeader('X-Marbles-Signature', responseSignature)
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64
import json

class CryptoService:
    def __init__(self, private_key_path: str, public_key_path: str):
        with open(private_key_path, 'rb') as f:
            self.private_key = serialization.load_pem_private_key(
                f.read(), password=None
            )
        with open(public_key_path, 'rb') as f:
            self.public_key = serialization.load_pem_public_key(f.read())

    def sign(self, message: str) -> str:
        signature = self.private_key.sign(
            message.encode(),
            padding.PKCS1v15(),
            hashes.SHA256()
        )
        return base64.b64encode(signature).decode()

    def is_valid(self, message: str, signature: str) -> bool:
        try:
            self.public_key.verify(
                base64.b64decode(signature),
                message.encode(),
                padding.PKCS1v15(),
                hashes.SHA256()
            )
            return True
        except Exception:
            return False

# Usage
crypto = CryptoService('your_priv.pem', 'marbles_pub.pem')  # marbles_pub.pem = 155.io's public key

# Verify an incoming 155.io callback
signature = request.headers['X-Marbles-Signature']
if not crypto.is_valid(raw_request_body, signature):
    return jsonify({'status': 'INVALID_SIGNATURE', 'requestId': request_id, 'clientPlayerId': player_id}), 200

# Optional: sign your response (we don't currently verify, but signing is forward-compatible)
response_signature = crypto.sign(json.dumps(response_body))
<?php

class CryptoService {
    private $privateKey;
    private $publicKey;

    public function __construct(string $privateKeyPath, string $publicKeyPath) {
        $this->privateKey = file_get_contents($privateKeyPath);
        $this->publicKey = file_get_contents($publicKeyPath);
    }

    public function sign(string $message): string {
        openssl_sign($message, $signature, $this->privateKey, OPENSSL_ALGO_SHA256);
        return base64_encode($signature);
    }

    public function isValid(string $message, string $signature): bool {
        return openssl_verify(
            $message,
            base64_decode($signature),
            $this->publicKey,
            OPENSSL_ALGO_SHA256
        ) === 1;
    }
}

// Usage — verify an incoming 155.io callback
$crypto = new CryptoService('your_priv.pem', 'marbles_pub.pem');
$signature = $_SERVER['HTTP_X_MARBLES_SIGNATURE'];
$rawBody = file_get_contents('php://input');

if (!$crypto->isValid($rawBody, $signature)) {
    http_response_code(200);
    echo json_encode(['status' => 'INVALID_SIGNATURE', 'requestId' => $requestId, 'clientPlayerId' => $playerId]);
    exit;
}

// Optional: sign your response
$responseSignature = $crypto->sign(json_encode($responseBody));
header('X-Marbles-Signature: ' . $responseSignature);
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.*;
import java.util.Base64;

public class CryptoService {
    private final PrivateKey privateKey;
    private final PublicKey publicKey;

    public CryptoService(String privateKeyPath, String publicKeyPath) throws Exception {
        this.privateKey = loadPrivateKey(privateKeyPath);
        this.publicKey = loadPublicKey(publicKeyPath);
    }

    public String sign(String message) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(message.getBytes());
        return Base64.getEncoder().encodeToString(signature.sign());
    }

    public boolean isValid(String message, String signatureStr) {
        try {
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initVerify(publicKey);
            signature.update(message.getBytes());
            return signature.verify(Base64.getDecoder().decode(signatureStr));
        } catch (Exception e) {
            return false;
        }
    }

    private PrivateKey loadPrivateKey(String path) throws Exception { /* PEM loader */ }
    private PublicKey loadPublicKey(String path) throws Exception { /* PEM loader */ }
}

// Usage — verify an incoming 155.io callback
CryptoService crypto = new CryptoService("your_priv.pem", "marbles_pub.pem");
String signature = request.getHeader("X-Marbles-Signature");
boolean isValid = crypto.isValid(rawRequestBody, signature);
using System;
using System.Security.Cryptography;
using System.Text;

public class CryptoService
{
    private readonly RSA _privateKey;
    private readonly RSA _publicKey;

    public CryptoService(string privateKeyPath, string publicKeyPath)
    {
        _privateKey = RSA.Create();
        _privateKey.ImportFromPem(File.ReadAllText(privateKeyPath));

        _publicKey = RSA.Create();
        _publicKey.ImportFromPem(File.ReadAllText(publicKeyPath));
    }

    public string Sign(string message)
    {
        byte[] data = Encoding.UTF8.GetBytes(message);
        byte[] signature = _privateKey.SignData(
            data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return Convert.ToBase64String(signature);
    }

    public bool IsValid(string message, string signature)
    {
        try
        {
            byte[] data = Encoding.UTF8.GetBytes(message);
            byte[] signatureBytes = Convert.FromBase64String(signature);
            return _publicKey.VerifyData(
                data, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        }
        catch
        {
            return false;
        }
    }
}

// Usage — verify an incoming 155.io callback
var crypto = new CryptoService("your_priv.pem", "marbles_pub.pem");
string signature = request.Headers["X-Marbles-Signature"];
bool isValid = crypto.IsValid(rawRequestBody, signature);

What we send and expect

SurfaceX-Marbles-Signature headerVerified 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, /rollbackAlways sent, RSA-SHA256 over raw body bytes.Yes — you must verify.
Your response to our /balance, /bet, /win, /rollbackOptional. Signing is forward-compatible but not currently checked.No (today)

Timeouts

The goal is sub-second communication between our systems. We set a 5-second timeout as a fallback in case a process fails to respond.

Idempotency

Important

All requests must be processed idempotently. If a request is executed multiple times, it should return the same response (even if the request ID differs).

If a duplicate transaction is detected with a different payload, return DUPLICATE_TRANSACTION_ERROR.

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.

On this page