Skip to main content

Error Codes & Handling

Complete reference for Loyalteez API error codes, common issues, and solutions.


Error Response Format

All Loyalteez APIs return errors in this standard format:

{
"error": "Error type",
"message": "Human-readable description",
"details": "Additional context (optional)",
"code": "ERROR_CODE (optional)"
}

Example Error:

{
"error": "Invalid event data",
"message": "Missing required field: eventType",
"details": ["eventType is required", "userEmail must be valid email format"]
}

HTTP Status Codes

StatusNameWhen It Occurs
200OKRequest succeeded
400Bad RequestInvalid input data
401UnauthorizedMissing/invalid auth token
403ForbiddenAccess denied (automation disabled, contract not whitelisted)
404Not FoundEndpoint doesn't exist
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer error (contact support)
503Service UnavailableService temporarily down

Event Handler Errors

400 - Invalid Event Data

Error:

{
"error": "Invalid event data",
"details": ["Missing required field: eventType"]
}

Cause: Missing or invalid required fields

Solution:

// ❌ Wrong
{
brandId: 'abc123',
// Missing eventType and userEmail
}

// ✅ Correct
{
brandId: 'abc123',
eventType: 'account_creation',
userEmail: '[email protected]'
}

400 - Missing Required Fields

Error:

{
"error": "Missing required fields",
"message": "brandId, eventType, and userEmail are required"
}

Required Fields:

  • brandId - Your Loyalteez Brand ID
  • eventType - Type of event (see supported events)
  • userEmail - User's email address

Solution:

const requiredFields = {
brandId: process.env.LOYALTEEZ_BRAND_ID,
eventType: 'account_creation',
userEmail: user.email
};

// Validate before sending
if (!requiredFields.brandId || !requiredFields.eventType || !requiredFields.userEmail) {
console.error('Missing required fields');
return;
}

await trackEvent(requiredFields);

400 - Event Type Not Configured

Error:

{
"error": "Event type not configured",
"message": "custom_event is not configured for rewards"
}

Cause: Using an event type that doesn't have a reward configured

Supported Event Types:

  • account_creation → 100 LTZ
  • complete_survey → 75 LTZ
  • newsletter_subscribe → 25 LTZ
  • rate_experience → 50 LTZ
  • subscribe_renewal → 200 LTZ
  • form_submit → 10 LTZ

Solution:

// ❌ Wrong - custom event type
eventType: 'my_custom_event'

// ✅ Correct - use supported event type
eventType: 'account_creation'

403 - Automation Disabled

Error:

{
"success": false,
"error": "Automation disabled",
"message": "Reward automation is currently disabled for this brand. Please enable it in your dashboard settings.",
"brandId": "your-brand-id"
}

Cause: Reward automation is turned off in Partner Portal

Solution:

  1. Log into Partner Portal
  2. Go to Settings → LTZ Distribution
  3. Toggle "Enable Automation" to ON
  4. Save settings

Handling in Code:

const response = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
body: JSON.stringify(eventData)
});

if (response.status === 403) {
const error = await response.json();
if (error.error === 'Automation disabled') {
// Show user-friendly message
console.warn('Rewards are currently disabled. Contact support.');
// Maybe queue the event for later?
}
}

429 - Rate Limit Exceeded

Error:

{
"error": "Rate limit exceeded",
"message": "Too many events from this email. Maximum 1 reward per event type per day per user."
}

Cause: User has already received reward for this event type today

Rate Limits:

  • Event Handler: 1 reward per event type per user per day
  • Gas Relayer: 35 transactions per user per hour
  • Pregeneration: 100 requests per brand per minute

Solution - Add Deduplication:

// Track which events user has already been rewarded for
const userRewardCache = new Map();

async function trackEventWithDedup(userEmail, eventType) {
const cacheKey = `${userEmail}-${eventType}-${getTodayDate()}`;

if (userRewardCache.has(cacheKey)) {
console.log('User already rewarded for this event today');
return { success: false, reason: 'already_rewarded' };
}

try {
const response = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
body: JSON.stringify({
brandId: 'your-brand-id',
eventType,
userEmail
})
});

if (response.status === 429) {
// Cache it to avoid future calls
userRewardCache.set(cacheKey, true);
return { success: false, reason: 'rate_limited' };
}

const data = await response.json();
userRewardCache.set(cacheKey, true);
return { success: true, data };

} catch (error) {
console.error('Event tracking error:', error);
return { success: false, reason: 'error', error };
}
}

function getTodayDate() {
return new Date().toISOString().split('T')[0]; // YYYY-MM-DD
}

500 - Internal Server Error

Error:

{
"error": "Internal server error",
"message": "Failed to process event"
}

Cause: Unexpected server error (database issue, blockchain issue, etc.)

Solution - Implement Retry Logic:

async function trackEventWithRetry(eventData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData)
});

if (response.status === 500) {
console.warn(`Attempt ${attempt}/${maxRetries} failed with 500 error`);

if (attempt < maxRetries) {
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
console.log(`Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

throw new Error('Max retries reached');
}

if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}

return await response.json();

} catch (error) {
if (attempt === maxRetries) {
console.error('Event tracking failed after retries:', error);
// Queue for later processing or alert ops team
await queueFailedEvent(eventData);
throw error;
}
}
}
}

503 - Service Unavailable

Error:

{
"error": "Database not configured - DNS verification not available"
}

Cause: Required service (database, blockchain) temporarily unavailable

Solution:

async function trackWithFallback(eventData) {
try {
return await trackEvent(eventData);
} catch (error) {
if (error.status === 503) {
// Queue event for later processing
console.warn('Service unavailable, queuing event');
await queueEventForLater(eventData);
return { success: false, queued: true };
}
throw error;
}
}

async function queueEventForLater(eventData) {
// Store in your database or queue system
await db.queuedEvents.create({
...eventData,
queuedAt: new Date(),
retryCount: 0
});
}

Gas Relayer Errors

401 - Invalid Privy Token

Error:

{
"error": "Invalid or expired Privy token"
}

Cause: Missing or expired Privy access token

Solution:

import { usePrivy } from '@privy-io/react-auth';

function MyComponent() {
const { getAccessToken, ready, authenticated } = usePrivy();

async function executeGaslessTransaction() {
// Check authentication first
if (!ready || !authenticated) {
console.error('User not authenticated');
return;
}

try {
// Get fresh token
const token = await getAccessToken();

if (!token) {
console.error('Failed to get access token');
return;
}

const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // ✅ Include token
},
body: JSON.stringify(transactionData)
});

if (response.status === 401) {
// Token expired, get new one
const newToken = await getAccessToken();
// Retry with new token
return executeGaslessTransaction();
}

return await response.json();

} catch (error) {
console.error('Transaction error:', error);
}
}
}

403 - Contract Not Whitelisted

Error:

{
"error": "Transaction validation failed: Contract not whitelisted"
}

Cause: Trying to call a contract that's not on the whitelist

Whitelisted Contracts:

  • Loyalteez Token (LTZ) - 0x5242b6DB88A72752ac5a54cFe6A7DB8244d743c9
  • PerkNFT - 0x6ae30d6Dcf3e75456B6582b057f1Bf98A90F2CA0
  • PointsSale - 0x5269B83F6A4E31bEdFDf5329DC052FBb661e3c72

Solution:

const WHITELISTED_CONTRACTS = {
LTZ_TOKEN: '0x5242b6DB88A72752ac5a54cFe6A7DB8244d743c9',
PERK_NFT: '0x6ae30d6Dcf3e75456B6582b057f1Bf98A90F2CA0',
POINTS_SALE: '0x5269B83F6A4E31bEdFDf5329DC052FBb661e3c72'
};

function validateContract(contractAddress) {
const whitelistedAddresses = Object.values(WHITELISTED_CONTRACTS);

if (!whitelistedAddresses.includes(contractAddress.toLowerCase())) {
throw new Error(`Contract ${contractAddress} is not whitelisted`);
}
}

// Use before calling gas relayer
validateContract(targetContract);

429 - Gas Relayer Rate Limit

Error:

{
"error": "Rate limit exceeded. Max 35 transactions per hour."
}

Cause: User has made 35+ gasless transactions in the last hour

Solution - Track Usage:

const userTransactionCount = new Map();

async function executeGaslessWithRateLimit(userAddress, txData) {
const hourKey = `${userAddress}-${getCurrentHour()}`;
const count = userTransactionCount.get(hourKey) || 0;

if (count >= 35) {
return {
success: false,
error: 'Rate limit reached',
message: 'You can make up to 35 gasless transactions per hour. Please try again later.',
resetAt: getNextHourTimestamp()
};
}

try {
const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
body: JSON.stringify(txData)
});

if (response.status === 429) {
return {
success: false,
error: 'Rate limit exceeded',
message: 'Transaction limit reached. Please try again in the next hour.'
};
}

const result = await response.json();

// Increment count
userTransactionCount.set(hourKey, count + 1);

return { success: true, ...result };

} catch (error) {
console.error('Transaction error:', error);
return { success: false, error: error.message };
}
}

function getCurrentHour() {
const now = new Date();
return `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}-${now.getHours()}`;
}

function getNextHourTimestamp() {
const nextHour = new Date();
nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
return nextHour.getTime();
}

Pregeneration Errors

400 - Invalid Discord ID

Error:

{
"error": "Invalid Discord user ID",
"details": "Discord user IDs must be 17-20 digit numeric strings (snowflake IDs)",
"example": "123456789012345678"
}

Cause: Discord user ID doesn't match snowflake format

Solution:

function validateDiscordId(userId) {
// Discord IDs are 17-20 digit numeric strings
if (!/^\d{17,20}$/.test(userId)) {
throw new Error('Invalid Discord ID format');
}
return userId;
}

// Use before calling pregeneration
const validDiscordId = validateDiscordId(discordUser.id);
await pregenerateWallet('discord', validDiscordId, discordUser.username);

400 - Invalid OAuth Credentials

Error:

{
"error": "Invalid OAuth credentials",
"hint": "This endpoint requires REAL OAuth user IDs and usernames from actual discord accounts. Test credentials are rejected by Privy for security.",
"provider": "discord"
}

Cause: Trying to use fake/test OAuth credentials

Solution:

// ❌ Wrong - test credentials
await pregenerateWallet('discord', 'test123', 'testuser');

// ✅ Correct - real Discord user ID from actual account
const realDiscordUser = await discordClient.users.fetch(userId);
await pregenerateWallet(
'discord',
realDiscordUser.id, // Real snowflake ID
realDiscordUser.username // Real username
);

Network Errors

CORS Errors

Error (Browser Console):

Access to fetch at 'https://api.loyalteez.app/...' from origin 'https://yoursite.com' 
has been blocked by CORS policy

Cause: CORS preflight failure or missing headers

Solution:

// Ensure correct headers
const response = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // ✅ Required
},
body: JSON.stringify(data)
});

Note: All Loyalteez APIs support CORS by default. If you still see CORS errors:

  1. Check you're using HTTPS (not HTTP)
  2. Verify the endpoint URL is correct
  3. Check browser console for actual error

Timeout Errors

Error:

Error: Request timeout after 30000ms

Cause: Request took too long (network issue, server busy)

Solution:

async function fetchWithTimeout(url, options, timeout = 30000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}

// Usage
try {
const response = await fetchWithTimeout(
'https://api.loyalteez.app/loyalteez-api/manual-event',
{
method: 'POST',
body: JSON.stringify(data)
},
10000 // 10 second timeout
);
} catch (error) {
if (error.message === 'Request timeout') {
console.error('Request took too long, retry later');
// Queue for retry
}
}

Error Handling Best Practices

1. Comprehensive Try-Catch

async function robustAPICall(endpoint, data) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

// Check HTTP status
if (!response.ok) {
const error = await response.json();

// Handle specific status codes
switch (response.status) {
case 400:
console.error('Invalid data:', error.details);
break;
case 403:
console.error('Access denied:', error.message);
break;
case 429:
console.warn('Rate limited, retry later');
break;
case 500:
console.error('Server error, will retry');
throw new Error('Retriable error');
default:
console.error('Unknown error:', error);
}

return { success: false, error };
}

return { success: true, data: await response.json() };

} catch (error) {
// Network errors, timeouts, etc.
console.error('Request failed:', error.message);
return { success: false, error: error.message };
}
}

2. Retry with Exponential Backoff

async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;

const delay = Math.min(1000 * Math.pow(2, i), 10000); // Max 10s
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

// Usage
await retryWithBackoff(async () => {
const response = await fetch(endpoint, options);
if (!response.ok) throw new Error('Request failed');
return response.json();
});

3. Error Logging & Monitoring

function logAPIError(endpoint, error, context) {
const errorLog = {
timestamp: new Date().toISOString(),
endpoint,
error: {
message: error.message,
status: error.status,
details: error.details
},
context,
userId: context.userId,
brandId: context.brandId
};

// Log to your monitoring service
console.error('[API Error]', JSON.stringify(errorLog));

// Send to error tracking (Sentry, etc.)
if (window.Sentry) {
window.Sentry.captureException(error, {
extra: errorLog
});
}
}

4. User-Friendly Error Messages

function getUserFriendlyMessage(error) {
const messages = {
400: 'Please check your input and try again.',
401: 'Please log in again to continue.',
403: 'You don\'t have permission to do this.',
429: 'You\'re doing that too often. Please wait a moment.',
500: 'Something went wrong on our end. We\'re looking into it.',
503: 'Service is temporarily unavailable. Please try again shortly.'
};

return messages[error.status] || 'An unexpected error occurred.';
}

// Usage
try {
await trackEvent(data);
} catch (error) {
const userMessage = getUserFriendlyMessage(error);
showNotification(userMessage, 'error');
}

Quick Reference

Common Issues Checklist

  • Invalid event data? → Check all required fields present
  • Rate limited? → Implement deduplication logic
  • Automation disabled? → Enable in Partner Portal settings
  • CORS error? → Verify using HTTPS and correct headers
  • 401 Unauthorized? → Refresh Privy token
  • Contract not whitelisted? → Use only whitelisted contracts
  • 500 error? → Implement retry logic with backoff

Support

Still Having Issues?

  1. Check Status: status.loyalteez.app
  2. Search Docs: docs.loyalteez.app
  3. Ask Community: Discord
  4. Contact Support: [email protected]

Include in Support Request:

  • Error message (full JSON)
  • Request timestamp
  • Endpoint called
  • Request body (remove sensitive data)
  • Your Brand ID