Rewards Catalog Complete Guide
The Phoenix Rewards Catalog provides a flexible reward system for managing purchasable rewards, automatic grants, and order fulfillment. This guide covers everything you need to integrate the rewards catalog into your application.
Authentication
All rewards catalog endpoints support multiple authentication methods:
| Gateway | Operation | Endpoints | Auth Methods |
|---|---|---|---|
| Admin Gateway | Create/manage rewards | /v1/tenants/{tenant_id}/rewards-catalog/* | Admin JWT, API Key |
| Admin Gateway | Grant rewards | /v1/tenants/{tenant_id}/grants | Admin JWT, API Key |
| Admin Gateway | Manage orders | /v1/tenants/{tenant_id}/reward-orders/* | Admin JWT, API Key |
| Query Gateway | List rewards | /v1/rewards/{tenant_id}/catalog | Tenant JWT, HMAC |
| Query Gateway | Get reward | /v1/rewards/{tenant_id}/catalog/{slug} | Tenant JWT, HMAC |
| Query Gateway | Purchase reward | /v1/rewards/{tenant_id}/purchase/{slug} | Tenant JWT, HMAC |
| Query Gateway | Claim order | /v1/rewards/{tenant_id}/orders/{order_id}/claim | Tenant JWT, HMAC |
| Query Gateway | List user orders | /v1/rewards/{tenant_id}/orders | Tenant JWT, HMAC |
Recommended for frontend apps: Use JWT authentication. Your proxy server issues JWTs after user login, and the frontend calls Phoenix APIs directly with the token.
See Getting Started for authentication setup details.
Key Features
- Reward Catalog: Define purchasable rewards with pricing and inventory
- Multiple Fulfillment Types: Wallet-based, manual, or API fulfillment
- Order Tracking: Full lifecycle tracking from purchase to delivery
- Custom Fields: Collect user information needed for fulfillment
- Stock Management: Inventory limits with automatic decrement
- Idempotent Purchases: Prevent duplicate orders with idempotency keys
Core Concepts
Rewards
Rewards are items users can purchase or receive as grants. Each reward includes:
| Property | Type | Description |
|---|---|---|
slug | string | Unique human-readable identifier |
name | string | Display name |
description | string | Optional description |
image_url | string | Optional image URL |
active | boolean | Whether reward is available |
fields | array | User input fields required for fulfillment |
fulfillment | object | How to deliver the reward |
requires_claim | boolean | Whether user must claim before fulfillment |
purchase | object | Purchase configuration with prices (optional) |
stock | integer | Inventory limit (null = unlimited) |
config | object | Extra data for fulfillment handler |
Fulfillment Types
Wallet Fulfillment
Automatically grants XP or badges via the wallet service:
{
"type": "wallet",
"item_type": "xp",
"amount": 1000
}{
"type": "wallet",
"item_type": "badge",
"item_id": "vip_badge_2026"
}Manual Fulfillment
For physical items requiring admin action:
{
"type": "manual",
"notes": "Ship to user's address"
}API Fulfillment
Calls your webhook for external fulfillment:
{
"type": "api",
"retry": {
"max_attempts": 3,
"backoff_seconds": [60, 300, 900]
}
}Order States
Orders progress through these states:
| State | 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 |
User Endpoints (Query Gateway)
List Purchasable Rewards
Get all active rewards available for purchase.
GET /v1/rewards/{tenant_id}/catalogQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
balance | integer | User's balance (optional, for affordability check) |
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",
"purchase": {
"prices": [
{ "currency": "gold_coins", "amount": 500 }
]
},
"stock": 100,
"fields": [
{
"name": "phone_number",
"label": "Phone Number",
"required": true,
"type": "text",
"pattern": "^\\+251[0-9]{9}$"
}
],
"affordable": true,
"points_needed": null
}
]
}Get Reward Details
Get details for a specific reward.
GET /v1/rewards/{tenant_id}/catalog/{slug}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 Reward
Purchase a reward using wallet currency.
POST /v1/rewards/{tenant_id}/purchase/{slug}Request:
{
"user_id": "user_123",
"idempotency_key": "purchase_user123_airtime_1706345678",
"currency": "gold_coins",
"fields": {
"phone_number": "+251912345678"
}
}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
}Claim Order
Submit required fields to claim a pending order (for grants with requires_claim: true).
POST /v1/rewards/{tenant_id}/orders/{order_id}/claimRequest:
{
"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"
}
}
}List User Orders
Get all orders for a user.
GET /v1/rewards/{tenant_id}/orders/{user_id}Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (optional) |
limit | integer | Page size (default: 20) |
offset | integer | Pagination offset |
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 Recent Field Values
Get auto-fill values from user's recent orders.
GET /v1/rewards/{tenant_id}/recent-fields?user_id={user_id}Response:
{
"fields": {
"phone_number": "+251912345678",
"address": "123 Main St, Addis Ababa"
}
}Admin Endpoints (Admin Gateway)
Create Reward
Create a new reward in the catalog.
POST /v1/tenants/{tenant_id}/rewards-catalogRequest:
{
"slug": "airtime-topup",
"name": "Airtime Top-up",
"description": "Mobile airtime for any network",
"image_url": "https://example.com/airtime.png",
"active": true,
"fields": [
{
"name": "phone_number",
"label": "Phone Number",
"required": true,
"type": "text",
"pattern": "^\\+251[0-9]{9}$",
"placeholder": "+251912345678"
}
],
"fulfillment": {
"type": "api",
"retry": {
"max_attempts": 3,
"backoff_seconds": [60, 300, 900]
}
},
"requires_claim": false,
"purchase": {
"prices": [
{ "currency": "gold_coins", "amount": 500 }
]
},
"stock": 1000,
"config": {
"amount_etb": 10,
"networks": ["ethio-telecom", "safaricom"]
}
}Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "your_tenant",
"slug": "airtime-topup",
"name": "Airtime Top-up",
"description": "Mobile airtime for any network",
"image_url": "https://example.com/airtime.png",
"active": true,
"fields": [...],
"fulfillment": {...},
"requires_claim": false,
"purchase": {
"prices": [
{ "currency": "gold_coins", "amount": 500 }
]
},
"stock": 1000,
"config": {
"amount_etb": 10,
"networks": ["ethio-telecom", "safaricom"]
},
"created_at": "2026-01-28T10:00:00Z",
"updated_at": "2026-01-28T10:00:00Z"
}List Rewards
List all rewards in the catalog.
GET /v1/tenants/{tenant_id}/rewards-catalogQuery Parameters:
| Parameter | Type | Description |
|---|---|---|
active | boolean | Filter by active status (default: true) |
limit | integer | Page size (default: 50) |
offset | integer | Pagination offset |
Get Reward
Get a specific reward by ID.
GET /v1/tenants/{tenant_id}/rewards-catalog/{reward_id}Update Reward
Update an existing reward.
PUT /v1/tenants/{tenant_id}/rewards-catalog/{reward_id}Request:
{
"name": "Airtime Top-up (Updated)",
"stock": 500,
"active": true
}All fields are optional. Only provided fields are updated.
Delete Reward
Deactivate (soft delete) a reward.
DELETE /v1/tenants/{tenant_id}/rewards-catalog/{reward_id}Grant Reward
Grant a reward to a user (bypass purchase).
POST /v1/tenants/{tenant_id}/grantsRequest:
{
"user_id": "user_123",
"reward_id": "550e8400-e29b-41d4-a716-446655440000",
"source_type": "leaderboard",
"source_ref": "weekly_top10",
"expires_at": "2026-02-28T23:59:59Z"
}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": "leaderboard",
"source_ref": "weekly_top10",
"status": {
"state": "pending_claim"
},
"fields": {},
"fulfillment": {...},
"requires_claim": true,
"expires_at": "2026-02-28T23:59:59Z",
"created_at": "2026-01-28T10:30:00Z",
"updated_at": "2026-01-28T10:30:00Z"
}
}Update Order Status (Manual Fulfillment)
Complete or fail a manual fulfillment order.
PUT /v1/tenants/{tenant_id}/reward-orders/{order_id}Complete:
{
"status": {
"state": "completed",
"tracking_number": "TRACK123456",
"shipped_at": "2026-01-28T14:00:00Z",
"notes": "Shipped via DHL"
}
}Failed:
{
"status": {
"state": "failed",
"reason": "Item out of stock"
}
}Retry Failed Order
Retry a failed order fulfillment.
POST /v1/tenants/{tenant_id}/reward-orders/{order_id}/retryRefund Order
Refund a purchase order.
POST /v1/tenants/{tenant_id}/reward-orders/{order_id}/refundRequest:
{
"reason": "Customer requested cancellation"
}JavaScript Integration Example
class RewardsCatalogClient {
constructor(apiUrl, jwt) {
this.apiUrl = apiUrl;
this.jwt = jwt;
}
async listRewards(tenantId, balance = null) {
const params = new URLSearchParams();
if (balance !== null) {
params.set('balance', balance);
}
const url = `${this.apiUrl}/v1/rewards/${tenantId}/catalog${params.toString() ? '?' + params : ''}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${this.jwt}`,
'Content-Type': 'application/json'
}
});
return response.json();
}
async getReward(tenantId, slug) {
const response = await fetch(
`${this.apiUrl}/v1/rewards/${tenantId}/catalog/${slug}`,
{
headers: {
'Authorization': `Bearer ${this.jwt}`,
'Content-Type': 'application/json'
}
}
);
return response.json();
}
async purchase(tenantId, slug, userId, currency, fields = {}) {
const response = await fetch(
`${this.apiUrl}/v1/rewards/${tenantId}/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(tenantId, userId, options = {}) {
const params = new URLSearchParams(options);
const response = await fetch(
`${this.apiUrl}/v1/rewards/${tenantId}/orders/${userId}?${params}`,
{
headers: {
'Authorization': `Bearer ${this.jwt}`,
'Content-Type': 'application/json'
}
}
);
return response.json();
}
async claim(tenantId, orderId, userId, fields) {
const response = await fetch(
`${this.apiUrl}/v1/rewards/${tenantId}/orders/${orderId}/claim`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${this.jwt}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ user_id: userId, fields })
}
);
return response.json();
}
async getRecentFields(tenantId, userId) {
const response = await fetch(
`${this.apiUrl}/v1/rewards/${tenantId}/recent-fields?user_id=${userId}`,
{
headers: {
'Authorization': `Bearer ${this.jwt}`,
'Content-Type': 'application/json'
}
}
);
return response.json();
}
}
// Example usage
const rewards = new RewardsCatalogClient('https://query.phoenix.example.com', userJwt);
// Browse catalog with affordability check
const { rewards: catalog } = await rewards.listRewards('tenant_abc', 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('tenant_abc', 'airtime-topup', 'user_123', 'gold_coins', {
phone_number: '+251912345678'
});
console.log(`Order created: ${order.order.id}`);
// Check order history
const { orders } = await rewards.listOrders('tenant_abc', 'user_123');
orders.forEach(o => {
console.log(`${o.reward_name}: ${o.status.state}`);
});Field Types
Rewards can require user input fields for fulfillment:
Text Field
{
"name": "phone_number",
"label": "Phone Number",
"required": true,
"type": "text",
"pattern": "^\\+[0-9]{10,15}$",
"min_length": 10,
"max_length": 15,
"placeholder": "+251912345678"
}Select Field
{
"name": "network",
"label": "Network Provider",
"required": true,
"type": "select",
"options": [
{ "value": "ethio", "label": "Ethio Telecom" },
{ "value": "safaricom", "label": "Safaricom" }
]
}Multiline Text
{
"name": "address",
"label": "Shipping Address",
"required": true,
"type": "text",
"multiline": true,
"max_length": 500
}Webhooks
Configure webhooks to receive reward order events.
Event Types
| Event | Description |
|---|---|
order.created | New order created (purchase or grant) |
order.claimed | User submitted claim fields |
order.fulfilling | Fulfillment started |
order.completed | Order successfully fulfilled |
order.failed | Order fulfillment failed |
Webhook Payload
{
"event_type": "order.completed",
"tenant_id": "your_tenant",
"timestamp": "2026-01-28T10:31:00Z",
"data": {
"order_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"user_id": "user_123",
"reward_id": "550e8400-e29b-41d4-a716-446655440000",
"reward_slug": "airtime-topup",
"fulfillment_type": "api",
"fulfillment_data": {
"external_id": "TXN123456"
}
}
}Webhooks include an X-Webhook-Signature header with an HMAC-SHA256 signature of the payload.
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 UX
Next Steps
- Wallet Complete Guide - Currency and balance management
- Streak Overview - Engagement tracking with reward grants