Phoenix Gamification
Coupon Codes

Redeeming Coupons

This guide explains how to query, validate, and redeem coupons for users via the Query Gateway.

Authentication

Query Gateway endpoints require tenant-level authentication:

GET /v1/coupons/{tenant_id}/user/{user_id}
Authorization: Bearer <tenant_jwt_token>

The JWT must contain in its payload:

  • sub_type: "tenant"
  • sub_id: {tenant_id}
  • Signed with the tenant HMAC secret

Option 2: HMAC Authentication (Server-to-Server)

GET /v1/coupons/{tenant_id}/user/{user_id}
X-Tenant-Id: {tenant_id}
X-Timestamp: {unix_timestamp}
X-Signature: hmac-sha256={hex_digest}

The signature is computed over the canonical string:

METHOD
PATH
TIMESTAMP
BODY

API Endpoints

List Coupon Definitions

Get all active coupon definitions for a tenant (useful for showing available promotions):

GET /v1/coupons/{tenant_id}/definitions
Authorization: Bearer <tenant_jwt>

Response:

{
  "definitions": [
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "tenant_id": "your-tenant-id",
      "name": "Welcome Bonus",
      "description": "Welcome bonus for new users",
      "config": {
        "reward_item_id": "550e8400-e29b-41d4-a716-446655440000",
        "code_format": "random",
        "code_prefix": "WELCOME",
        "code_length": 8,
        "case_sensitive": false,
        "usage_limit": {"limit_type": "per_user", "max_uses": 1}
      },
      "status": "active"
    }
  ],
  "total": 1
}

Get User's Coupons

Get all coupons assigned to a user:

GET /v1/coupons/{tenant_id}/user/{user_id}
Authorization: Bearer <tenant_jwt>

Response:

{
  "coupons": [
    {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "definition_id": "660e8400-e29b-41d4-a716-446655440001",
      "definition_name": "Welcome Bonus",
      "tenant_id": "your-tenant-id",
      "user_id": "user_123",
      "code": "WELCOME8X4F2K9P",
      "status": "active",
      "generated_at": "2025-01-15T10:00:00Z",
      "expires_at": "2025-12-31T23:59:59Z",
      "redeemed_at": null,
      "redemption_count": 0,
      "coupon_type": "reward",
      "reward_item": {
        "item_id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "100 Bonus Points",
        "description": "Grants 100 bonus points to the user",
        "reward_type": "points",
        "payload": {"amount": 100}
      }
    }
  ],
  "total": 1
}

Get Specific Coupon

GET /v1/coupons/{tenant_id}/user/{user_id}/{coupon_id}
Authorization: Bearer <tenant_jwt>

Response: Same structure as individual coupon above.

Validating Coupons

Before redemption, validate that a coupon code is valid:

GET /v1/coupons/{tenant_id}/validate/{code}
Authorization: Bearer <tenant_jwt>

Valid Coupon Response

{
  "valid": true,
  "coupon": {
    "id": "770e8400-e29b-41d4-a716-446655440002",
    "definition_id": "660e8400-e29b-41d4-a716-446655440001",
    "definition_name": "Welcome Bonus",
    "tenant_id": "your-tenant-id",
    "user_id": "user_123",
    "code": "WELCOME8X4F2K9P",
    "status": "active",
    "generated_at": "2025-01-15T10:00:00Z",
    "expires_at": "2025-12-31T23:59:59Z",
    "redeemed_at": null,
    "redemption_count": 0,
    "coupon_type": "reward",
    "reward_item": {...}
  },
  "definition": null,
  "error": null
}

Invalid Coupon Response

{
  "valid": false,
  "coupon": null,
  "definition": null,
  "error": "Coupon has expired"
}

Common Validation Errors

Error MessageMeaning
"Coupon code not found"No coupon with this code exists for this tenant
"Coupon has expired"Past the expires_at date
"Coupon not yet active"Before the starts_at date
"Coupon has already been used"Already redeemed (for single-use coupons)
"Coupon status: redeemed"Status is not active
"Coupon status: revoked"Coupon was revoked by admin

Redeeming Coupons

Redeem a coupon code:

POST /v1/coupons/{tenant_id}/redeem/{code}
Authorization: Bearer <tenant_jwt>
Content-Type: application/json

{
  "user_id": "user_123"
}

Response:

{
  "id": "770e8400-e29b-41d4-a716-446655440002",
  "definition_id": "660e8400-e29b-41d4-a716-446655440001",
  "definition_name": "Welcome Bonus",
  "tenant_id": "your-tenant-id",
  "user_id": "user_123",
  "code": "WELCOME8X4F2K9P",
  "status": "redeemed",
  "generated_at": "2025-01-15T10:00:00Z",
  "expires_at": "2025-12-31T23:59:59Z",
  "redeemed_at": "2025-01-15T14:30:00Z",
  "redemption_count": 1,
  "coupon_type": "reward",
  "reward_item": {
    "item_id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "100 Bonus Points",
    "description": "Grants 100 bonus points to the user",
    "reward_type": "points",
    "payload": {"amount": 100}
  }
}

Redemption Process

  1. Validation: The system checks:

    • Coupon exists and belongs to this tenant
    • Status is active
    • Not expired (past expires_at)
    • Not before start date (before starts_at)
    • Usage limits not exceeded
  2. Redemption: If valid:

    • Status updated to redeemed
    • redeemed_at timestamp set
    • redemption_count incremented
    • Event published for downstream processing
  3. Error Handling: If invalid:

    • Returns appropriate error message
    • Coupon remains unchanged

Redemption Errors

HTTP StatusErrorMeaning
404"Coupon not found"No coupon with this code exists
400"Coupon has expired"Past expiration date
400"Coupon not yet active"Before start date
400"Coupon has already been redeemed"Already used
400"Usage limit exceeded"Max uses reached

Redemption History

Get a user's redemption history:

GET /v1/coupons/{tenant_id}/user/{user_id}/redemptions?limit=100
Authorization: Bearer <tenant_jwt>

Response:

{
  "redemptions": [
    {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "definition_id": "660e8400-e29b-41d4-a716-446655440001",
      "definition_name": "Welcome Bonus",
      "tenant_id": "your-tenant-id",
      "user_id": "user_123",
      "code": "WELCOME8X4F2K9P",
      "status": "redeemed",
      "generated_at": "2025-01-15T10:00:00Z",
      "expires_at": "2025-12-31T23:59:59Z",
      "redeemed_at": "2025-01-15T14:30:00Z",
      "redemption_count": 1,
      "coupon_type": "reward",
      "reward_item": {...}
    }
  ],
  "total": 1
}

Case Sensitivity

By default, coupon codes are case-insensitive:

  • WELCOME8X4F2K9P = welcome8x4f2k9p = Welcome8X4F2K9P

If a coupon definition has case_sensitive: true, exact case matching is required.

Example Integration

JavaScript/TypeScript (Frontend with JWT)

// Your proxy server issues this JWT after user login
const jwtToken = await getAuthToken();

const tenantId = 'your-tenant-id';
const userId = 'user_123';

// Get user's coupons
async function getUserCoupons() {
  const response = await fetch(
    `${QUERY_API_URL}/v1/coupons/${tenantId}/user/${userId}`,
    {
      headers: {
        'Authorization': `Bearer ${jwtToken}`
      }
    }
  );
  return response.json();
}

// Validate a coupon code
async function validateCoupon(code) {
  const response = await fetch(
    `${QUERY_API_URL}/v1/coupons/${tenantId}/validate/${code}`,
    {
      headers: {
        'Authorization': `Bearer ${jwtToken}`
      }
    }
  );
  
  const result = await response.json();
  
  if (!result.valid) {
    throw new Error(result.error);
  }
  
  return result.coupon;
}

// Redeem a coupon
async function redeemCoupon(code) {
  const response = await fetch(
    `${QUERY_API_URL}/v1/coupons/${tenantId}/redeem/${code}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${jwtToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ user_id: userId })
    }
  );
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Redemption failed');
  }
  
  return response.json();
}

// Usage
try {
  // First validate
  const coupon = await validateCoupon('WELCOME8X4F2K9P');
  console.log('Coupon is valid:', coupon.reward_item.name);
  
  // Then redeem
  const redeemed = await redeemCoupon('WELCOME8X4F2K9P');
  console.log('Redeemed! Reward:', redeemed.reward_item);
} catch (error) {
  console.error('Coupon error:', error.message);
}

Python (Server-to-Server with HMAC)

import hmac
import hashlib
import time
import requests

QUERY_API_URL = 'https://query.api.yourgame.gg'
TENANT_ID = 'your-tenant-id'
TENANT_SECRET = 'your-tenant-secret'

def generate_signature(method: str, path: str, timestamp: str, body: str = '') -> str:
    canonical = f"{method}\n{path}\n{timestamp}\n{body}"
    signature = hmac.new(
        TENANT_SECRET.encode(),
        canonical.encode(),
        hashlib.sha256
    ).hexdigest()
    return f"hmac-sha256={signature}"

def get_user_coupons(user_id: str) -> dict:
    path = f'/v1/coupons/{TENANT_ID}/user/{user_id}'
    timestamp = str(int(time.time()))
    signature = generate_signature('GET', path, timestamp)
    
    response = requests.get(
        f'{QUERY_API_URL}{path}',
        headers={
            'X-Tenant-Id': TENANT_ID,
            'X-Timestamp': timestamp,
            'X-Signature': signature
        }
    )
    response.raise_for_status()
    return response.json()

def redeem_coupon(user_id: str, code: str) -> dict:
    path = f'/v1/coupons/{TENANT_ID}/redeem/{code}'
    timestamp = str(int(time.time()))
    body = f'{{"user_id": "{user_id}"}}'
    signature = generate_signature('POST', path, timestamp, body)
    
    response = requests.post(
        f'{QUERY_API_URL}{path}',
        headers={
            'X-Tenant-Id': TENANT_ID,
            'X-Timestamp': timestamp,
            'X-Signature': signature,
            'Content-Type': 'application/json'
        },
        data=body
    )
    response.raise_for_status()
    return response.json()

# Usage
coupons = get_user_coupons('user_123')
print(f"User has {coupons['total']} coupons")

if coupons['coupons']:
    code = coupons['coupons'][0]['code']
    result = redeem_coupon('user_123', code)
    print(f"Redeemed: {result['reward_item']['name']}")

Creating Tenant JWTs (Proxy Server)

Your proxy server issues JWTs after authenticating users:

const jwt = require('jsonwebtoken');

function createTenantJWT(tenantId, tenantSecret, userId, expiresIn = 3600) {
  const now = Math.floor(Date.now() / 1000);
  return jwt.sign({
    sub: userId,
    sub_type: 'tenant',
    sub_id: tenantId,
    iat: now,
    exp: now + expiresIn,
  }, tenantSecret, { algorithm: 'HS256' });
}

// Issue token when user logs in
app.post('/login', async (req, res) => {
  const user = await authenticateUser(req.body);
  const token = createTenantJWT(TENANT_ID, TENANT_SECRET, user.id);
  res.json({ token });
});

Best Practices

  1. Always Validate First: Validate coupon codes before attempting redemption to give users clear feedback
  2. Handle Errors Gracefully: Check the valid field and display meaningful error messages
  3. Show Expiration Dates: Display expiration dates in the UI so users know urgency
  4. Track Redemptions: Use redemption history for user account pages and analytics
  5. Normalize Input: Even though codes are case-insensitive by default, consider trimming whitespace from user input
  6. Cache Definitions: Cache the definitions list to show available promotions without repeated API calls

Error Codes Reference

HTTP StatusMeaning
200Success
400Invalid request (expired, already redeemed, usage limit, etc.)
401Authentication failed (invalid JWT or HMAC)
403Access denied (wrong tenant)
404Coupon or user not found
500Internal server error