Phoenix Gamification
Rewards

Querying Rewards

This guide explains how users can browse the reward catalog, view reward details, purchase rewards, and track their orders.

This guide explains how users can browse the reward catalog, view reward details, purchase rewards, and track their orders.

Overview

The Query API provides endpoints for:

  • Listing available rewards in the catalog
  • Getting details for a specific reward
  • Purchasing rewards
  • Viewing order history
  • Claiming pending orders

All endpoints require authentication. See Getting Started for authentication setup.

List Available Rewards

Get all active rewards available in the catalog.

Endpoint:

GET /v1/rewards/catalog

Query Parameters:

ParameterTypeDescription
balanceintegerUser's balance (optional, for affordability check)
purchasablebooleanFilter by purchasable status (optional)

Example Request:

GET /v1/rewards/catalog?balance=1500&purchasable=true

Response:

{
  "rewards": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "slug": "airtime-topup",
      "name": "Airtime Top-up",
      "description": "Mobile airtime for any network",
      "image_url": "https://example.com/airtime.png",
      "fields": [
        {
          "name": "phone_number",
          "label": "Phone Number",
          "required": true,
          "type": "text",
          "pattern": "^\\+251[0-9]{9}$"
        }
      ],
      "purchase": {
        "prices": [
          { "currency": "gold_coins", "amount": 500 }
        ]
      },
      "stock": 100,
      "affordable": true,
      "points_needed": null
    },
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "slug": "xp-boost-100",
      "name": "100 XP Boost",
      "description": "Instant 100 XP boost",
      "image_url": "https://example.com/xp-boost.png",
      "fields": [],
      "purchase": {
        "prices": [
          { "currency": "gold_coins", "amount": 50 }
        ]
      },
      "stock": null,
      "affordable": true,
      "points_needed": null
    }
  ]
}

Affordability Check: If you provide the balance parameter, the response includes:

  • affordable: true if user has enough currency, false otherwise
  • points_needed: How many more points needed if not affordable (only for POINTS currency)

Get Reward Details

Get detailed information about a specific reward by its slug.

Endpoint:

GET /v1/rewards/catalog/{slug}

Example Request:

GET /v1/rewards/catalog/airtime-topup

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "slug": "airtime-topup",
  "name": "Airtime Top-up",
  "description": "Mobile airtime for any network",
  "image_url": "https://example.com/airtime.png",
  "fields": [
    {
      "name": "phone_number",
      "label": "Phone Number",
      "required": true,
      "type": "text",
      "pattern": "^\\+251[0-9]{9}$"
    }
  ],
  "purchase": {
    "prices": [
      { "currency": "gold_coins", "amount": 500 }
    ]
  },
  "stock": 100
}

Purchase a Reward

Purchase a reward using wallet currency.

Endpoint:

POST /v1/rewards/purchase/{slug}

Request Body:

{
  "user_id": "user_123",
  "idempotency_key": "purchase_user123_airtime_1706345678",
  "currency": "gold_coins",
  "fields": {
    "phone_number": "+251912345678"
  }
}

Fields:

  • user_id - The user making the purchase
  • idempotency_key - Unique key to prevent duplicate purchases (use timestamp or UUID)
  • currency - Which currency to use (must match one of the reward's prices)
  • fields - Required field values if the reward needs user information

Response:

{
  "order": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "tenant_id": "your_tenant",
    "user_id": "user_123",
    "reward_id": "550e8400-e29b-41d4-a716-446655440000",
    "variant_id": "default",
    "source_type": "purchase",
    "status": {
      "state": "fulfilling",
      "started_at": "2026-01-28T10:30:00Z"
    },
    "fields": {
      "phone_number": "+251912345678"
    },
    "fulfillment": {
      "type": "api"
    },
    "requires_claim": false,
    "created_at": "2026-01-28T10:30:00Z",
    "updated_at": "2026-01-28T10:30:00Z"
  },
  "was_cached": false
}

Error Responses:

  • 400 - Invalid request (missing fields, invalid currency, etc.)
  • 404 - Reward not found
  • 402 - Insufficient balance or payment failed
  • 409 - Out of stock or duplicate purchase

List User Orders

Get all orders for a user (both purchases and grants).

Endpoint:

GET /v1/rewards//orders/{user_id}

Query Parameters:

ParameterTypeDescription
statusstringFilter by status (optional)
limitintegerPage size (default: 20)
offsetintegerPagination offset

Example Request:

GET /v1/rewards/orders/user_123?status=completed&limit=10

Response:

{
  "orders": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "reward_id": "550e8400-e29b-41d4-a716-446655440000",
      "reward_name": "Airtime Top-up",
      "reward_image_url": "https://example.com/airtime.png",
      "variant_id": "default",
      "source_type": "purchase",
      "status": {
        "state": "completed",
        "data": {
          "type": "api",
          "external_id": "TXN123456"
        },
        "completed_at": "2026-01-28T10:31:00Z"
      },
      "requires_claim": false,
      "created_at": "2026-01-28T10:30:00Z"
    }
  ],
  "total": 15,
  "limit": 20,
  "offset": 0
}

Get Order Details

Get detailed information about a specific order.

Endpoint:

GET /v1/rewards/orders/{user_id}/{order_id}

Response:

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "tenant_id": "your_tenant",
  "user_id": "user_123",
  "reward_id": "550e8400-e29b-41d4-a716-446655440000",
  "variant_id": "default",
  "source_type": "purchase",
  "source_ref": null,
  "status": {
    "state": "completed",
    "data": {
      "type": "api",
      "external_id": "TXN123456"
    },
    "completed_at": "2026-01-28T10:31:00Z"
  },
  "fields": {
    "phone_number": "+251912345678"
  },
  "fulfillment": {
    "type": "api"
  },
  "requires_claim": false,
  "expires_at": null,
  "created_at": "2026-01-28T10:30:00Z",
  "updated_at": "2026-01-28T10:31:00Z"
}

Claim a Pending Order

If a reward requires user information (like shipping address), users must claim the order and provide the required fields.

Endpoint:

POST /v1/rewards/orders/{order_id}/claim

Request Body:

{
  "user_id": "user_123",
  "fields": {
    "phone_number": "+251912345678",
    "address": "123 Main St, Addis Ababa"
  }
}

Response:

{
  "order": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "status": {
      "state": "fulfilling",
      "started_at": "2026-01-28T10:35:00Z"
    }
  }
}

Get Recent Field Values

Get auto-fill values from user's recent orders. Useful for pre-filling forms.

Endpoint:

GET /v1/rewards/recent-fields?user_id={user_id}

Response:

{
  "fields": {
    "phone_number": "+251912345678",
    "address": "123 Main St, Addis Ababa"
  }
}

Order Statuses

Orders progress through these states:

StatusDescription
pending_claimWaiting for user to provide required fields
fulfillingBeing processed
completedSuccessfully delivered
failedDelivery failed (may be retryable)
expiredExpired before claiming

JavaScript Example

class RewardsClient {
  constructor(apiUrl, jwt) {
    this.apiUrl = apiUrl;
    this.jwt = jwt;
  }

  async listRewards( balance = null) {
    const params = new URLSearchParams();
    if (balance !== null) {
      params.set('balance', balance);
    }
    
    const url = `${this.apiUrl}/v1/rewards/catalog${params.toString() ? '?' + params : ''}`;
    const response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${this.jwt}`,
        'Content-Type': 'application/json'
      }
    });
    return response.json();
  }

  async getReward( slug) {
    const response = await fetch(
      `${this.apiUrl}/v1/rewards/catalog/${slug}`,
      {
        headers: {
          'Authorization': `Bearer ${this.jwt}`,
          'Content-Type': 'application/json'
        }
      }
    );
    return response.json();
  }

  async purchase( slug, userId, currency, fields = {}) {
    const response = await fetch(
      `${this.apiUrl}/v1/rewards/purchase/${slug}`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.jwt}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          user_id: userId,
          idempotency_key: `purchase_${userId}_${slug}_${Date.now()}`,
          currency,
          fields
        })
      }
    );
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Purchase failed');
    }
    
    return response.json();
  }

  async listOrders( userId, options = {}) {
    const params = new URLSearchParams(options);
    
    const response = await fetch(
      `${this.apiUrl}/v1/rewards/orders/${userId}?${params}`,
      {
        headers: {
          'Authorization': `Bearer ${this.jwt}`,
          'Content-Type': 'application/json'
        }
      }
    );
    return response.json();
  }

  async claim( orderId, userId, fields) {
    const response = await fetch(
      `${this.apiUrl}/v1/rewards/orders/${orderId}/claim`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.jwt}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ user_id: userId, fields })
      }
    );
    return response.json();
  }
}

// Example usage
const rewards = new RewardsClient('https://query.phoenix.example.com', userJwt);

// Browse catalog with affordability check
const { rewards: catalog } = await rewards.listRewards( 1500);
console.log(`Available rewards: ${catalog.length}`);

// Show affordable rewards
catalog.filter(r => r.affordable).forEach(r => {
  console.log(`${r.name} - ${r.purchase?.prices[0]?.amount} coins`);
});

// Purchase airtime
const order = await rewards.purchase( 'airtime-topup', 'user_123', 'gold_coins', {
  phone_number: '+251912345678'
});
console.log(`Order created: ${order.order.id}`);

// Check order history
const { orders } = await rewards.listOrders( 'user_123');
orders.forEach(o => {
  console.log(`${o.reward_name}: ${o.status.state}`);
});

Best Practices

  1. Idempotency Keys: Always generate unique keys for purchases to prevent duplicates
  2. Field Validation: Validate fields client-side before submission
  3. Stock Monitoring: Monitor stock levels and disable purchases when low
  4. Error Handling: Handle insufficient balance and out-of-stock gracefully
  5. Recent Fields: Use the recent-fields endpoint for auto-fill
  6. Order Polling: Poll order status for long-running fulfillments
  7. Affordability Check: Pass user balance to catalog endpoint for better user experience

Next Steps

On this page