Spin the Wheel Overview
Phoenix provides a flexible spin-the-wheel gamification feature that allows users to spin for rewards with configurable probability weights and frequency limits.
Spin-the-wheel lets you create wheel games where users can spin to win rewards. Each wheel has:
- Segments: Multiple segments with different reward items (or no-win segments)
- Probabilities: Weighted chances for each segment
- Frequency Limits: Configurable limits (daily, total, cooldown, or unlimited)
- Date Ranges: Optional start/end dates for time-limited wheels
Client Endpoints
All client endpoints require JWT or HMAC authentication.
Get Available Wheels
Retrieve all active wheels available for a user.
GET /v1/wheels/{tenant_id}Response:
{
"wheels": [
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "Daily Spin",
"description": "Spin once per day for rewards",
"config": {
"segments": [
{
"reward_item_id": "item-gems-100",
"probability": 3,
"label": "100 Gems"
},
{
"reward_item_id": null,
"probability": 5,
"label": "Try Again"
},
{
"reward_item_id": "item-coins-50",
"probability": 2,
"label": "50 Coins"
}
],
"frequency": {
"type": "daily_limit",
"value": 1
},
"starts_at": "2025-01-01T00:00:00Z",
"ends_at": "2025-12-31T23:59:59Z"
}
}
]
}Spin the Wheel
Spin a wheel for a user. The result is determined by weighted random selection.
POST /v1/wheels/{tenant_id}/{wheel_id}/spinRequest Body:
{
"user_id": "user_123"
}Response:
{
"segment_index": 0,
"segment": {
"reward_item_id": "item-gems-100",
"probability": 3,
"label": "100 Gems"
},
"reward_item": {
"item_id": "item-gems-100",
"name": "100 Gems",
"description": "100 premium gems",
"reward_type": "currency",
"payload": {
"currency": "gems",
"amount": 100
}
}
}Error Responses:
400- Wheel not active, expired, not started, or frequency limit exceeded404- Wheel not found
Get User Spin History
Retrieve a user's spin history.
GET /v1/wheels/{tenant_id}/user/{user_id}/historyQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
wheel_id | UUID | Filter by specific wheel (optional) |
limit | integer | Page size (default: 100, max: 1000) |
offset | integer | Page offset (default: 0) |
Response:
{
"spins": [
{
"id": "spin-123",
"wheel_id": "wheel-abc",
"result_index": 0,
"reward_item_id": "item-gems-100",
"spun_at": "2025-01-04T10:30:00Z"
},
{
"id": "spin-124",
"wheel_id": "wheel-abc",
"result_index": 1,
"reward_item_id": null,
"spun_at": "2025-01-03T15:45:00Z"
}
],
"total": 25
}JavaScript Example
// Get available wheels
async function getAvailableWheels(tenantId) {
const response = await fetch(
`${QUERY_API_URL}/v1/wheels/${tenantId}`,
{
headers: {
'Authorization': `Bearer ${jwt}`,
'Content-Type': 'application/json'
}
}
);
return response.json();
}
// Spin a wheel
async function spinWheel(tenantId, wheelId, userId) {
const response = await fetch(
`${QUERY_API_URL}/v1/wheels/${tenantId}/${wheelId}/spin`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ user_id: userId })
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to spin wheel');
}
return response.json();
}
// Example: Display wheel and handle spin
const { wheels } = await getAvailableWheels('tenant_abc');
const dailyWheel = wheels[0];
const result = await spinWheel('tenant_abc', dailyWheel.id, 'user_123');
if (result.reward_item) {
console.log(`You won: ${result.reward_item.name}!`);
} else {
console.log('Better luck next time!');
}Wheel Configuration
Segments
Each segment represents one slice of the wheel:
{
"reward_item_id": "uuid-or-null", // null = no-win segment
"probability": 3, // Weight (higher = more likely)
"label": "100 Gems" // Optional display label
}Probability Calculation:
- Probabilities are relative weights, not percentages
- Example:
[3, 5, 2]means segment 2 is 5/10 = 50% likely, segment 1 is 3/10 = 30%, segment 3 is 2/10 = 20%
Frequency Limits
Control how often users can spin:
| Type | Description | Value Meaning |
|---|---|---|
unlimited | No limits | value ignored |
daily_limit | Max spins per day | value = max spins (resets at midnight UTC) |
total_limit | Max spins ever | value = max spins per user |
cooldown | Time between spins | value = hours to wait |
Example Configurations:
// Daily limit: 3 spins per day
{
"type": "daily_limit",
"value": 3
}
// Total limit: 10 spins ever
{
"type": "total_limit",
"value": 10
}
// Cooldown: 4 hours between spins
{
"type": "cooldown",
"value": 4
}
// Unlimited
{
"type": "unlimited"
}Date Ranges
Optional time-based availability:
{
"starts_at": "2025-01-01T00:00:00Z", // Wheel not available before this
"ends_at": "2025-12-31T23:59:59Z" // Wheel not available after this
}Admin Endpoints
Admins can manage wheel definitions via the admin API.
Create Wheel
POST /v1/tenants/{tenant_id}/wheelsRequest:
{
"name": "Daily Spin",
"description": "Spin once per day",
"config": {
"segments": [
{ "reward_item_id": "item-gems-100", "probability": 3, "label": "100 Gems" },
{ "reward_item_id": null, "probability": 5, "label": "Try Again" }
],
"frequency": {
"type": "daily_limit",
"value": 1
}
}
}List Wheels
GET /v1/tenants/{tenant_id}/wheelsUpdate Wheel
PUT /v1/tenants/{tenant_id}/wheels/{wheel_id}Delete Wheel
DELETE /v1/tenants/{tenant_id}/wheels/{wheel_id}Reward Tracking
Winning spins are automatically tracked in ClickHouse via the Reward Grants API. You can query all wheel rewards using:
GET /v1/rewards/{tenant_id}/user/{user_id}?source_type=wheelBest Practices
- Probability Design: Use simple ratios (e.g., 1, 2, 3) rather than percentages for easier adjustment
- No-Win Segments: Include segments with
reward_item_id: nullto create "try again" experiences - Frequency Limits: Start with daily limits to encourage daily engagement
- Date Ranges: Use for seasonal or promotional wheels
- Error Handling: Always handle frequency limit errors gracefully in your UI