One integration.
Every game we make.
A seamless-wallet REST API. You keep the player's balance; PlayEola calls your wallet for every bet, win, and rollback. HMAC-signed, idempotent, and typed end to end.
Overview
The Operator Integration API connects your platform to the PlayEola game runtime. It runs in two directions — the calls you make to us, and the wallet calls we make to you.
You Call
Launch game sessions, verify credentials, and list the games available to a brand. Every request is HMAC-signed.
game-launch · auth · games/list · health PlayEola calls youYou Implement
Expose a wallet endpoint set. We call it to read balance and to debit, credit, and roll back transactions — always idempotent.
balance · debit · credit · rollbackQuickstart
Authenticate
Call POST /integration/auth with a signed empty body to confirm your API key and secret resolve to your entity.
Launch a game
Call POST /games/game-launch with a player context. You get a single-use launch_url — open it exactly as returned.
Serve wallet calls
Implement /balance, /debit, /credit, and /rollback. We call them during play; you settle against the player's real balance.
pe_test_, production keys pe_live_. The base URL https://api.playeola.com is the same for both — the key selects the environment.Authentication
Requests are signed with HMAC-SHA256. Every request except GET /health carries three headers.
| Header | Req | Description |
|---|---|---|
| X-API-Key | yes | Your API key — pe_live_… (prod) or pe_test_… (test) |
| X-PlayEola-Signature | yes | HMAC-SHA256 signature, hex-encoded |
| X-PlayEola-Timestamp | yes | ISO-8601 timestamp |
| Content-Type | POST | application/json |
Operator → PlayEola. Concatenate the timestamp and the compact JSON body, then HMAC with your secret:
PlayEola → your wallet. Wallet calls also fold the endpoint path into the signed payload:
- Sign compact JSON — no extra whitespace. For GET requests, sign an empty body
{}. - The timestamp must be within 5 minutes of server time.
- The server verifies against
JSON.stringify(parsedBody)— number formatting must match JS:0.50→0.5,500.00→500.
JSON.stringify does or the signature won't match. Never pad decimals or send trailing zeros in a signed body.# sign timestamp + compact-JSON body with your secret
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
BODY='{"brand_code":"MAIN","game_code":"Clockwork_Tyrant","context":{"player_id":"player_12345","currency":"USD"}}'
SIG=$(printf '%s' "$TS$BODY" \
| openssl dgst -sha256 -hmac "$PE_API_SECRET" \
| sed 's/^.* //')
curl -X POST https://api.playeola.com/games/game-launch \
-H "Content-Type: application/json" \
-H "X-API-Key: $PE_API_KEY" \
-H "X-PlayEola-Signature: $SIG" \
-H "X-PlayEola-Timestamp: $TS" \
-d "$BODY"// sign.js — reusable signed client (Node 18+)
import crypto from "node:crypto";
const API_KEY = process.env.PE_API_KEY;
const API_SECRET = process.env.PE_API_SECRET;
const BASE_URL = "https://api.playeola.com";
export async function signedFetch(method, path, body) {
const timestamp = new Date().toISOString();
const payload = body ? JSON.stringify(body) : "{}";
const signature = crypto
.createHmac("sha256", API_SECRET)
.update(timestamp + payload)
.digest("hex");
const res = await fetch(BASE_URL + path, {
method,
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
"X-PlayEola-Signature": signature,
"X-PlayEola-Timestamp": timestamp
},
body: method === "GET" ? undefined : payload
});
return res.json();
}// sign.ts — typed signed client (Node 18+)
import crypto from "node:crypto";
const API_KEY = process.env.PE_API_KEY!;
const API_SECRET = process.env.PE_API_SECRET!;
const BASE_URL = "https://api.playeola.com";
export async function signedFetch<T>(
method: "GET" | "POST",
path: string,
body?: unknown
): Promise<T> {
const timestamp = new Date().toISOString();
const payload = body ? JSON.stringify(body) : "{}";
const signature = crypto
.createHmac("sha256", API_SECRET)
.update(timestamp + payload)
.digest("hex");
const res = await fetch(BASE_URL + path, {
method,
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
"X-PlayEola-Signature": signature,
"X-PlayEola-Timestamp": timestamp
},
body: method === "GET" ? undefined : payload
});
return res.json() as Promise<T>;
}Response conventions
Successful responses carry "status": "success" alongside the endpoint's data. Errors arrive in one of two envelope shapes — integrations must handle both and ignore unknown fields.
| Code | Meaning |
|---|---|
| 200 | OK |
| 202 | Accepted |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 429 | Too Many Requests |
| 500 | Internal Error |
{
"status": "success"
// …endpoint data
}
{
"status": "error",
"type": "AUTHENTICATION_ERROR",
"message": "Invalid signature",
"context": {}
}
{
"status": "error",
"error": {
"type": "VALIDATION_ERROR",
"message": "brand_code is required"
}
}
Errors
Two envelopes, one contract: read status first, then find type and message either at the top level (flat) or inside error (nested).
| Shape | Used by | Where to read type / message |
|---|---|---|
| Flat | 401 auth · 403 "Integration is not active" | type + message at top level, optional context |
| Nested | 400 · 404 · other 403 · 500 | type + message inside an error object |
Rate limits
Limits are applied per IP. Authenticated endpoints return standard RateLimit-* headers so you can pace yourself.
| Category | Limit | Window |
|---|---|---|
/integration/auth | 100 req | 1 min |
| Bet processing (game client) | 600 req | 1 min |
| General (game-launch, games/list, …) | 1000 req | 1 min |
{
"status": "error",
"error": {
"type": "RATE_LIMIT_ERROR",
"message": "Too many requests, please try again later"
}
}