S2S Operator API
The Server-to-Server (S2S) Operator API is used by Predictu administrators to manage casino operators programmatically. It provides endpoints for creating, updating, and configuring operators, managing their API keys, and testing callback connectivity. All endpoints require God Mode authentication unless otherwise noted.
https://app.predictu.com/api/s2sAuthentication: God Mode session token in
Authorization: Bearer <token> header.Content-Type: All requests and responses use
application/json.god_mode role. Operator admin users cannot access these endpoints. This API is for Predictu internal use to manage the B2B operator lifecycle.Operator Data Model
Before diving into the endpoints, here is the complete operator data model that is returned and accepted across the API:
{
"id": "op_abc123",
"name": "Example Casino",
"domain": "example-casino.com",
"status": "active",
"contact_email": "admin@example-casino.com",
"billing_email": "billing@example-casino.com",
"callback_url": "https://api.example-casino.com/callbacks/predictu",
"callback_status": "healthy",
"callback_last_success": "2025-03-18T14:30:00.000Z",
"callback_failure_count": 0,
"revenue_share_predictu": 30,
"revenue_share_operator": 70,
"allowed_categories": ["politics", "sports", "crypto", "entertainment"],
"branding": {
"primary_color": "#1a7a3a",
"secondary_color": "#0d5a25",
"bg_primary": "#0a120a",
"bg_secondary": "#0f1a0f",
"logo_url": "https://cdn.predictu.com/operators/op_abc123/logo.png",
"icon_url": "https://cdn.predictu.com/operators/op_abc123/icon.png"
},
"config": {
"new_tier_limit": 10,
"regular_tier_limit": 100,
"vip_tier_limit": 1000,
"auto_promote_enabled": true,
"auto_promote_days": 7,
"auto_promote_trades": 5,
"daily_loss_limit_enabled": false,
"can_promote_vip": false
},
"onboarding_step": 8,
"onboarding_complete": true,
"allowed_origins": [
"https://www.example-casino.com",
"https://staging.example-casino.com"
],
"user_count": 1247,
"active_user_count_24h": 89,
"total_volume": 245000.00,
"mtd_revenue": 12400.00,
"created_at": "2025-01-10T09:00:00.000Z",
"updated_at": "2025-03-18T14:30:00.000Z"
}List All Operators
Returns a paginated list of all operators on the platform. Supports filtering by status and searching by name or domain.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by status: "active", "inactive", "onboarding", or "all" (default: "all") |
search | string | No | Search by operator name or domain (partial match) |
sort | string | No | Sort field: "name", "created", "volume", "users" (default: "created") |
order | string | No | "asc" or "desc" (default: "desc") |
limit | number | No | Pagination limit (default: 20, max: 100) |
offset | number | No | Pagination offset (default: 0) |
Response (200 OK)
{
"operators": [
{
"id": "op_abc123",
"name": "Example Casino",
"domain": "example-casino.com",
"status": "active",
"user_count": 1247,
"active_user_count_24h": 89,
"total_volume": 245000.00,
"mtd_revenue": 12400.00,
"revenue_share_predictu": 30,
"onboarding_complete": true,
"created_at": "2025-01-10T09:00:00.000Z"
},
{
"id": "op_def456",
"name": "LuckySpin",
"domain": "luckyspin.io",
"status": "onboarding",
"user_count": 0,
"active_user_count_24h": 0,
"total_volume": 0.00,
"mtd_revenue": 0.00,
"revenue_share_predictu": 25,
"onboarding_complete": false,
"created_at": "2025-03-15T11:00:00.000Z"
}
],
"total": 2,
"limit": 20,
"offset": 0
}Create Operator
Creates a new operator record and initializes the onboarding process. The operator starts in"onboarding" status and must complete the 8-step checklist before being activated.
Request Body
{
"name": "LuckySpin Casino",
"domain": "luckyspin.io",
"contact_email": "admin@luckyspin.io",
"billing_email": "billing@luckyspin.io",
"revenue_share_predictu": 25,
"allowed_categories": ["politics", "sports", "crypto", "entertainment"],
"config": {
"new_tier_limit": 15,
"regular_tier_limit": 200,
"vip_tier_limit": 2000,
"auto_promote_enabled": true,
"auto_promote_days": 5,
"auto_promote_trades": 3
}
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Operator display name. Must be unique. 3–100 characters. |
domain | string | Yes | Primary domain. Used for display and default origin whitelisting. |
contact_email | string | Yes | Primary contact email for the operator. |
billing_email | string | No | Billing email. Defaults to contact_email if not provided. |
revenue_share_predictu | number | Yes | Predictu’s revenue share percentage (1–99). Operator gets the remainder. |
allowed_categories | string[] | No | Market categories this operator can access. Defaults to all. |
config | object | No | Operator-specific configuration overrides. See Operator Config. |
Response (201 Created)
{
"success": true,
"operator": {
"id": "op_new789",
"name": "LuckySpin Casino",
"domain": "luckyspin.io",
"status": "onboarding",
"contact_email": "admin@luckyspin.io",
"billing_email": "billing@luckyspin.io",
"revenue_share_predictu": 25,
"revenue_share_operator": 75,
"onboarding_step": 1,
"onboarding_complete": false,
"created_at": "2025-03-18T15:00:00.000Z"
}
}Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid fields |
| 409 | NAME_EXISTS | An operator with this name already exists |
| 409 | DOMAIN_EXISTS | An operator with this domain already exists |
Side Effects
- An
operator_onboardingrecord is created with step 1 marked as complete - The audit log records the creation with the God Mode admin’s user ID
- Default branding values are initialized (Predictu green theme)
- An empty
allowed_originslist is created
Get Operator Detail
Returns the complete detail of a single operator, including all configuration, branding, onboarding status, and summary statistics.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Operator ID (e.g., op_abc123) |
Response (200 OK)
Returns the full operator object as described in the Operator Data Model section above, plus additional computed fields:
{
// ... all fields from the operator data model, plus:
"api_keys": [
{
"key_id": "key_abc123",
"public_key": "pk_live_abc123...",
"status": "active",
"created_at": "2025-01-10T09:30:00.000Z",
"last_used_at": "2025-03-18T14:30:00.000Z"
}
],
"onboarding_checklist": {
"create_record": { "complete": true, "completed_at": "2025-01-10T09:00:00.000Z" },
"configure_revenue": { "complete": true, "completed_at": "2025-01-10T09:05:00.000Z" },
"generate_keys": { "complete": true, "completed_at": "2025-01-10T09:10:00.000Z" },
"set_callback": { "complete": true, "completed_at": "2025-01-10T09:15:00.000Z" },
"test_ping": { "complete": true, "completed_at": "2025-01-10T09:20:00.000Z" },
"configure_branding": { "complete": true, "completed_at": "2025-01-10T09:30:00.000Z" },
"whitelist_origins": { "complete": true, "completed_at": "2025-01-10T09:35:00.000Z" },
"activate": { "complete": true, "completed_at": "2025-01-10T10:00:00.000Z" }
},
"recent_invoices": [
{
"invoice_id": "inv_abc",
"period": "2025-02",
"gross_revenue": 8500.00,
"predictu_share": 2550.00,
"operator_share": 5950.00,
"status": "paid"
}
]
}Error Responses
| Status | Code | Description |
|---|---|---|
| 404 | NOT_FOUND | No operator found with this ID |
Update Operator
Updates an existing operator’s configuration, contact details, branding, or status. Only the fields included in the request body are updated; all other fields remain unchanged.
Request Body
Any combination of the following fields. All are optional -include only what you want to change.
{
"name": "Example Casino (Updated)",
"contact_email": "new-admin@example-casino.com",
"billing_email": "new-billing@example-casino.com",
"status": "active",
"callback_url": "https://api-v2.example-casino.com/callbacks/predictu",
"revenue_share_predictu": 35,
"allowed_categories": ["politics", "sports"],
"allowed_origins": [
"https://www.example-casino.com",
"https://app.example-casino.com"
],
"branding": {
"primary_color": "#2ecc71"
},
"config": {
"new_tier_limit": 20,
"can_promote_vip": true
}
}| Field | Type | Description |
|---|---|---|
name | string | Operator display name |
contact_email | string | Primary contact email |
billing_email | string | Billing email |
status | string | "active", "inactive", or "onboarding" |
callback_url | string | S2S callback endpoint (must be HTTPS) |
revenue_share_predictu | number | Predictu’s revenue share % (1–99) |
allowed_categories | string[] | Market categories accessible to this operator |
allowed_origins | string[] | Whitelisted iframe origins (replaces entire list) |
branding | object | Partial branding update (merged with existing) |
config | object | Partial config update (merged with existing) |
Response (200 OK)
{
"success": true,
"operator": {
// Full updated operator object
"id": "op_abc123",
"name": "Example Casino (Updated)",
"status": "active",
"revenue_share_predictu": 35,
"revenue_share_operator": 65,
"updated_at": "2025-03-18T16:00:00.000Z",
// ... all other fields with current values
},
"changes": {
"name": { "from": "Example Casino", "to": "Example Casino (Updated)" },
"revenue_share_predictu": { "from": 30, "to": 35 },
"config.can_promote_vip": { "from": false, "to": true }
}
}The changes object in the response provides a diff of what actually changed, making it easy to verify the update and for audit purposes.
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid field values |
| 404 | NOT_FOUND | No operator found with this ID |
| 409 | NAME_EXISTS | New name conflicts with another operator |
"inactive"immediately blocks all new trades for that operator’s users. Existing open positions remain until resolved. Changing back to "active" restores trading. All status changes are logged and trigger an S2S callback to the operator (if their callback URL is configured).Audit Trail
Every field change is recorded in the audit log with the admin who made the change, the timestamp, and the before/after values. Sensitive changes (status, revenue share, config) also generate a notification to all God Mode admins.
Generate API Keys
Generates a new API key pair (public key + secret key) for the operator. If the operator already has active keys, the old keys enter a 24-hour grace period before expiring.
Request Body
{
"label": "Production keys v2",
"grace_period_hours": 24
}| Field | Type | Required | Description |
|---|---|---|---|
label | string | No | Human-readable label for the key pair. Defaults to "API Key". |
grace_period_hours | number | No | Hours before old keys expire (default: 24, min: 1, max: 72). |
Response (201 Created)
{
"success": true,
"key_pair": {
"key_id": "key_new456",
"public_key": "pk_live_xyz789abc...",
"secret_key": "sk_live_secret123...",
"label": "Production keys v2",
"status": "active",
"created_at": "2025-03-18T16:00:00.000Z"
},
"previous_keys": {
"key_id": "key_abc123",
"status": "expiring",
"expires_at": "2025-03-19T16:00:00.000Z"
}
}secret_key is returned only oncein this response. It is never stored in plaintext and cannot be retrieved again. If lost, the operator must generate a new key pair. Copy the secret key immediately and store it securely.Grace Period Behavior
When new keys are generated while old keys are active:
- The new keys become active immediately
- Old keys are marked as
"expiring"with anexpires_attimestamp - Both old and new keys are accepted during the grace period
- After the grace period, old keys are permanently deactivated
- A
KEY_ROTATEDevent is logged in the audit trail
Get Public Key
Returns the operator’s current active public key. This is a convenience endpoint used during embed integration setup and debugging. Unlike other S2S endpoints, this one does notrequire God Mode authentication -it returns only public information.
Response (200 OK)
{
"operator_id": "op_abc123",
"public_key": "pk_live_xyz789abc...",
"key_id": "key_new456",
"status": "active",
"created_at": "2025-03-18T16:00:00.000Z"
}Error Responses
| Status | Code | Description |
|---|---|---|
| 404 | NOT_FOUND | No operator found with this ID |
| 404 | NO_ACTIVE_KEY | Operator exists but has no active API key |
Test Callback (PING)
Sends a test PING callback to the operator’s configured callback URL and reports the result. This is used during onboarding to verify that the operator’s server can receive and respond to Predictu callbacks.
Request Body
{
"callback_url": "https://api.example-casino.com/callbacks/predictu",
"timeout_ms": 10000
}| Field | Type | Required | Description |
|---|---|---|---|
callback_url | string | No | URL to test. Defaults to the operator’s currently configured callback_url. Useful for testing a new URL before saving it. |
timeout_ms | number | No | Timeout in milliseconds (default: 10000, max: 30000). |
PING Callback Payload
The following payload is sent to the operator’s callback URL as a POST request:
// Request sent TO the operator's callback URL:
POST https://api.example-casino.com/callbacks/predictu
Content-Type: application/json
X-Predictu-Signature: sha256=abc123...
X-Predictu-Event: PING
X-Predictu-Delivery: dlv_test_789
{
"event": "PING",
"operator_id": "op_abc123",
"timestamp": "2025-03-18T16:05:00.000Z",
"test": true,
"payload": {
"message": "This is a test callback from Predictu. Respond with HTTP 200 to confirm receipt."
}
}Expected Operator Response
The operator’s server must respond with HTTP 200 and the following body:
{
"status": "ok",
"received_event": "PING"
}API Response (200 OK)
{
"success": true,
"test_result": {
"callback_url": "https://api.example-casino.com/callbacks/predictu",
"http_status": 200,
"response_body": {
"status": "ok",
"received_event": "PING"
},
"latency_ms": 142,
"signature_sent": "sha256=abc123...",
"tls_version": "TLSv1.3",
"ip_resolved": "203.0.113.42"
}
}Failure Response (200 OK, but test failed)
Note: Even when the test fails, the API itself returns 200. The failure details are in the response body.
{
"success": false,
"test_result": {
"callback_url": "https://api.example-casino.com/callbacks/predictu",
"error": "TIMEOUT",
"error_message": "Callback did not respond within 10000ms",
"latency_ms": 10000,
"suggestion": "Ensure the callback URL is accessible and the server responds within the timeout period. Check firewall rules and DNS resolution."
}
}Possible Test Errors
| Error | Description | Suggestion |
|---|---|---|
TIMEOUT | Server did not respond within the timeout | Check server availability and network connectivity |
CONNECTION_REFUSED | TCP connection was refused | Verify the server is running and the port is correct |
DNS_RESOLUTION_FAILED | Could not resolve the hostname | Check the domain name in the callback URL |
TLS_ERROR | SSL/TLS handshake failed | Ensure valid SSL certificate (self-signed not accepted) |
HTTP_ERROR | Server returned a non-200 status code | Check server logs for the callback handler |
INVALID_RESPONSE | Server returned 200 but response body was not valid JSON or missing required fields | Ensure response includes status and received_event |
NOT_HTTPS | Callback URL does not use HTTPS | HTTPS is required for all callback URLs |
Callback Signature Verification
All callbacks sent by Predictu include a signature header that operators should verify to ensure the callback is authentic.
Signature Format
X-Predictu-Signature: sha256=<hex-encoded HMAC-SHA256>The signature is computed over the raw JSON request body using the operator’s secret key:
// Verification pseudocode:
const signature = hmac_sha256(operator_secret_key, request_body_string);
const expected = request.headers['X-Predictu-Signature'];
const is_valid = timing_safe_equals(
'sha256=' + hex_encode(signature),
expected
);Callback Event Types
Beyond PING, the following callback events are sent during normal operation:
| Event | Description | Payload |
|---|---|---|
PING | Test/health check | Message string |
USER_REGISTERED | New user created via embed | User ID, external player ID, timestamp |
TRADE_PLACED | User placed a trade | Trade details (market, side, amount, price) |
POSITION_RESOLVED | A user’s position was settled | Position details, outcome, payout |
USER_RESTRICTED | User was auto-restricted by scoring | User ID, score, classification |
BALANCE_CHANGED | User balance changed (deposit/withdrawal/payout) | User ID, previous balance, new balance, reason |
MARKET_RESOLVED | A market was resolved | Market ID, outcome, affected user count |
Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
GET /operators | 60 requests | Per minute |
POST /operators | 10 requests | Per minute |
GET /operators/[id] | 60 requests | Per minute |
PATCH /operators/[id] | 30 requests | Per minute |
POST /operators/[id]/keys | 5 requests | Per hour |
GET /operators/[id]/public-key | 120 requests | Per minute |
POST /operators/[id]/test | 10 requests | Per minute |
