Redeeming Coupons
How to query, validate, and redeem coupons for users via the Query Gateway.
Redeeming Coupons
This guide explains how to query, validate, and redeem coupons for users via the Query Gateway.
Authentication
Query Gateway uses JWT only. Send Authorization: Bearer <JWT>. Tenant and user are resolved from the token; do not include tenant_id in the URL path.
API Endpoints
List Coupon Definitions
Get all active coupon definitions for the tenant (from JWT):
GET /v1/coupons/definitions
Authorization: Bearer <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/user/{user_id}
Authorization: Bearer <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/user/{user_id}/{coupon_id}
Authorization: Bearer <JWT>Response: Same structure as individual coupon above.
Validating Coupons
Before redemption, validate that a coupon code is valid:
GET /v1/coupons/validate/{code}
Authorization: Bearer <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/redeem/{code}
Authorization: Bearer <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/user/{user_id}/redemptions?limit=100
Authorization: Bearer <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 userId = 'user_123'; // tenant from JWT
// Get user's coupons
async function getUserCoupons() {
const response = await fetch(
`${QUERY_API_URL}/v1/coupons/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/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/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 JWT)
Query Gateway uses JWT only. Obtain a token (e.g. from your IdP or gamer-token endpoint) and send it in the Authorization header:
import requests
QUERY_API_URL = 'https://query.api.yourgame.gg'
JWT = 'your-jwt-token' # tenant/user from token
def get_user_coupons(user_id: str) -> dict:
response = requests.get(
f'{QUERY_API_URL}/v1/coupons/user/{user_id}',
headers={'Authorization': f'Bearer {JWT}'}
)
response.raise_for_status()
return response.json()
def redeem_coupon(user_id: str, code: str) -> dict:
response = requests.post(
f'{QUERY_API_URL}/v1/coupons/redeem/{code}',
headers={
'Authorization': f'Bearer {JWT}',
'Content-Type': 'application/json'
},
json={'user_id': user_id}
)
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 |