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:
Option 1: Tenant JWT (Recommended for Frontend)
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
BODYAPI 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 Message | Meaning |
|---|---|
"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
-
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
-
Redemption: If valid:
- Status updated to
redeemed redeemed_attimestamp setredemption_countincremented- Event published for downstream processing
- Status updated to
-
Error Handling: If invalid:
- Returns appropriate error message
- Coupon remains unchanged
Redemption Errors
| HTTP Status | Error | Meaning |
|---|---|---|
| 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
- Always Validate First: Validate coupon codes before attempting redemption to give users clear feedback
- Handle Errors Gracefully: Check the
validfield and display meaningful error messages - Show Expiration Dates: Display expiration dates in the UI so users know urgency
- Track Redemptions: Use redemption history for user account pages and analytics
- Normalize Input: Even though codes are case-insensitive by default, consider trimming whitespace from user input
- Cache Definitions: Cache the definitions list to show available promotions without repeated API calls
Error Codes Reference
| HTTP Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Invalid request (expired, already redeemed, usage limit, etc.) |
| 401 | Authentication failed (invalid JWT or HMAC) |
| 403 | Access denied (wrong tenant) |
| 404 | Coupon or user not found |
| 500 | Internal server error |
Creating Coupon Codes
This guide explains how to create coupon definitions and generate coupons for users via the Admin Gateway.
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.