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/catalogQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
balance | integer | User's balance (optional, for affordability check) |
purchasable | boolean | Filter by purchasable status (optional) |
Example Request:
GET /v1/rewards/catalog?balance=1500&purchasable=trueResponse:
{
"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:trueif user has enough currency,falseotherwisepoints_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-topupResponse:
{
"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 purchaseidempotency_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 found402- Insufficient balance or payment failed409- 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:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (optional) |
limit | integer | Page size (default: 20) |
offset | integer | Pagination offset |
Example Request:
GET /v1/rewards/orders/user_123?status=completed&limit=10Response:
{
"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}/claimRequest 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:
| Status | Description |
|---|---|
pending_claim | Waiting for user to provide required fields |
fulfilling | Being processed |
completed | Successfully delivered |
failed | Delivery failed (may be retryable) |
expired | Expired 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
- Idempotency Keys: Always generate unique keys for purchases to prevent duplicates
- Field Validation: Validate fields client-side before submission
- Stock Monitoring: Monitor stock levels and disable purchases when low
- Error Handling: Handle insufficient balance and out-of-stock gracefully
- Recent Fields: Use the recent-fields endpoint for auto-fill
- Order Polling: Poll order status for long-running fulfillments
- Affordability Check: Pass user balance to catalog endpoint for better user experience
Next Steps
- Creating Rewards - Learn how to create rewards
- Rewards Overview - Understand the rewards system