Node.js Backend Integration
Complete Node.js backend integration with Loyalteez APIs.
🎯 For Backend Developers: Server-side event tracking, user management, and reward distribution.
Prerequisites
- Node.js 16+ installed
- Loyalteez Brand ID (Get yours)
Installation
npm install express axios dotenv
Environment Setup
Create .env file:
# Loyalteez Configuration
LOYALTEEZ_BRAND_ID=your_brand_id
LOYALTEEZ_API_URL=https://api.loyalteez.app
# Server Configuration
PORT=3000
NODE_ENV=development
Project Structure
src/
├── index.js # Express server
├── routes/
│ ├── events.js # Event tracking routes
│ └── webhooks.js # Webhook handlers
├── services/
│ └── loyalteez.js # Loyalteez API client
├── middleware/
│ └── errorHandler.js # Error handling
└── config/
└── constants.js # Configuration
1. Configuration
src/config/constants.js
require('dotenv').config();
module.exports = {
LOYALTEEZ: {
BRAND_ID: process.env.LOYALTEEZ_BRAND_ID,
API_URL: process.env.LOYALTEEZ_API_URL || 'https://api.loyalteez.app',
},
SERVER: {
PORT: process.env.PORT || 3000,
NODE_ENV: process.env.NODE_ENV || 'development',
},
EVENT_TYPES: {
ACCOUNT_CREATION: 'account_creation',
COMPLETE_SURVEY: 'complete_survey',
NEWSLETTER_SUBSCRIBE: 'newsletter_subscribe',
RATE_EXPERIENCE: 'rate_experience',
SUBSCRIBE_RENEWAL: 'subscribe_renewal',
FORM_SUBMIT: 'form_submit',
},
REWARDS: {
ACCOUNT_CREATION: 100,
COMPLETE_SURVEY: 75,
NEWSLETTER_SUBSCRIBE: 25,
RATE_EXPERIENCE: 50,
SUBSCRIBE_RENEWAL: 200,
FORM_SUBMIT: 10,
},
};
2. Loyalteez API Client
src/services/loyalteez.js
const axios = require('axios');
const { LOYALTEEZ } = require('../config/constants');
/**
* Loyalteez API Client
*/
class LoyalteezClient {
constructor(brandId, apiUrl) {
this.brandId = brandId;
this.apiUrl = apiUrl;
// Create axios instance with defaults
this.client = axios.create({
baseURL: `${apiUrl}/loyalteez-api`,
headers: {
'Content-Type': 'application/json',
},
timeout: 30000, // 30 second timeout
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => {
console.error('Loyalteez API Error:', {
status: error.response?.status,
message: error.response?.data?.message,
details: error.response?.data,
});
throw error;
}
);
}
/**
* Track an event and reward user
*/
async trackEvent(eventType, userEmail, metadata = {}) {
try {
const response = await this.client.post('/manual-event', {
brandId: this.brandId,
eventType,
userEmail,
userIdentifier: userEmail,
metadata: {
source: 'nodejs_backend',
timestamp: new Date().toISOString(),
...metadata,
},
});
console.log('✅ Event tracked:', {
eventType,
userEmail,
eventId: response.data.eventId,
reward: response.data.rewardAmount,
});
return response.data;
} catch (error) {
// Handle specific error cases
if (error.response?.status === 429) {
console.warn('⚠️ Rate limited - user already rewarded for this event');
return { success: false, reason: 'rate_limited' };
}
if (error.response?.status === 403) {
console.warn('⚠️ Automation disabled for brand');
return { success: false, reason: 'automation_disabled' };
}
throw error;
}
}
/**
* Track multiple events in batch
*/
async trackEventsBatch(events) {
const results = await Promise.allSettled(
events.map(event =>
this.trackEvent(event.eventType, event.userEmail, event.metadata)
)
);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`✅ Batch complete: ${successful} successful, ${failed} failed`);
return {
total: events.length,
successful,
failed,
results,
};
}
/**
* Check API health
*/
async healthCheck() {
try {
const response = await this.client.get('/health');
return response.data;
} catch (error) {
return { status: 'unhealthy', error: error.message };
}
}
}
// Export singleton instance
module.exports = new LoyalteezClient(
LOYALTEEZ.BRAND_ID,
LOYALTEEZ.API_URL
);
3. Error Handling Middleware
src/middleware/errorHandler.js
/**
* Global error handler middleware
*/
function errorHandler(err, req, res, next) {
console.error('Error:', err);
// Default error
let status = 500;
let message = 'Internal server error';
let details = null;
// Handle specific error types
if (err.response) {
// Axios error (from Loyalteez API)
status = err.response.status;
message = err.response.data?.message || err.message;
details = err.response.data?.details;
} else if (err.name === 'ValidationError') {
// Validation error
status = 400;
message = 'Validation failed';
details = err.errors;
}
res.status(status).json({
success: false,
error: message,
details,
timestamp: new Date().toISOString(),
});
}
module.exports = errorHandler;
4. Event Tracking Routes
src/routes/events.js
const express = require('express');
const router = express.Router();
const loyalteez = require('../services/loyalteez');
const { EVENT_TYPES } = require('../config/constants');
/**
* Track a single event
* POST /api/events/track
*/
router.post('/track', async (req, res, next) => {
try {
const { eventType, userEmail, metadata } = req.body;
// Validate input
if (!eventType || !userEmail) {
return res.status(400).json({
success: false,
error: 'Missing required fields: eventType, userEmail',
});
}
// Validate event type
if (!Object.values(EVENT_TYPES).includes(eventType)) {
return res.status(400).json({
success: false,
error: 'Invalid event type',
validTypes: Object.values(EVENT_TYPES),
});
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userEmail)) {
return res.status(400).json({
success: false,
error: 'Invalid email format',
});
}
// Track event
const result = await loyalteez.trackEvent(eventType, userEmail, metadata);
res.json({
success: true,
...result,
});
} catch (error) {
next(error);
}
});
/**
* Track multiple events in batch
* POST /api/events/batch
*/
router.post('/batch', async (req, res, next) => {
try {
const { events } = req.body;
if (!Array.isArray(events) || events.length === 0) {
return res.status(400).json({
success: false,
error: 'Events array is required',
});
}
// Validate each event
for (const event of events) {
if (!event.eventType || !event.userEmail) {
return res.status(400).json({
success: false,
error: 'Each event must have eventType and userEmail',
});
}
}
// Track events in batch
const result = await loyalteez.trackEventsBatch(events);
res.json({
success: true,
...result,
});
} catch (error) {
next(error);
}
});
/**
* Get event types and rewards
* GET /api/events/types
*/
router.get('/types', (req, res) => {
const { EVENT_TYPES, REWARDS } = require('../config/constants');
const eventInfo = Object.keys(EVENT_TYPES).map(key => ({
type: EVENT_TYPES[key],
reward: REWARDS[key] || 0,
description: getEventDescription(EVENT_TYPES[key]),
}));
res.json({
success: true,
events: eventInfo,
});
});
function getEventDescription(eventType) {
const descriptions = {
account_creation: 'User creates a new account',
complete_survey: 'User completes a survey',
newsletter_subscribe: 'User subscribes to newsletter',
rate_experience: 'User rates their experience',
subscribe_renewal: 'User renews subscription',
form_submit: 'User submits a form',
};
return descriptions[eventType] || '';
}
module.exports = router;
5. Webhook Routes
src/routes/webhooks.js
const express = require('express');
const router = express.Router();
const loyalteez = require('../services/loyalteez');
const { EVENT_TYPES } = require('../config/constants');
/**
* Stripe webhook handler
* POST /webhooks/stripe
*/
router.post('/stripe', async (req, res, next) => {
try {
const event = req.body;
console.log('Stripe webhook received:', event.type);
// Handle different Stripe events
switch (event.type) {
case 'customer.created':
await handleCustomerCreated(event.data.object);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
await handleSubscriptionEvent(event.data.object);
break;
default:
console.log('Unhandled event type:', event.type);
}
res.json({ received: true });
} catch (error) {
next(error);
}
});
async function handleCustomerCreated(customer) {
const userEmail = customer.email;
if (userEmail) {
await loyalteez.trackEvent(
EVENT_TYPES.ACCOUNT_CREATION,
userEmail,
{
source: 'stripe_webhook',
customerId: customer.id,
}
);
}
}
async function handleSubscriptionEvent(subscription) {
const userEmail = subscription.customer?.email;
if (userEmail && subscription.status === 'active') {
await loyalteez.trackEvent(
EVENT_TYPES.SUBSCRIBE_RENEWAL,
userEmail,
{
source: 'stripe_webhook',
subscriptionId: subscription.id,
plan: subscription.items?.data[0]?.price?.id,
}
);
}
}
/**
* Generic webhook handler for other platforms
* POST /webhooks/generic
*/
router.post('/generic', async (req, res, next) => {
try {
const { event_type, user_email, metadata } = req.body;
// Map external event types to Loyalteez event types
const eventTypeMap = {
'user.signup': EVENT_TYPES.ACCOUNT_CREATION,
'user.survey_complete': EVENT_TYPES.COMPLETE_SURVEY,
'user.newsletter': EVENT_TYPES.NEWSLETTER_SUBSCRIBE,
'user.rating': EVENT_TYPES.RATE_EXPERIENCE,
};
const loyalteezEventType = eventTypeMap[event_type];
if (!loyalteezEventType) {
return res.status(400).json({
success: false,
error: 'Unknown event type',
});
}
await loyalteez.trackEvent(loyalteezEventType, user_email, metadata);
res.json({ success: true });
} catch (error) {
next(error);
}
});
module.exports = router;
6. Express Server
src/index.js
const express = require('express');
const { SERVER } = require('./config/constants');
const loyalteez = require('./services/loyalteez');
const eventsRouter = require('./routes/events');
const webhooksRouter = require('./routes/webhooks');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// Health check
app.get('/health', async (req, res) => {
const loyalteezHealth = await loyalteez.healthCheck();
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
services: {
loyalteez: loyalteezHealth.status,
},
});
});
// Routes
app.use('/api/events', eventsRouter);
app.use('/webhooks', webhooksRouter);
// Root endpoint
app.get('/', (req, res) => {
res.json({
name: 'Loyalteez Backend Integration',
version: '1.0.0',
endpoints: [
'GET /health',
'GET /api/events/types',
'POST /api/events/track',
'POST /api/events/batch',
'POST /webhooks/stripe',
'POST /webhooks/generic',
],
});
});
// Error handler (must be last)
app.use(errorHandler);
// Start server
app.listen(SERVER.PORT, () => {
console.log(`🚀 Server running on port ${SERVER.PORT}`);
console.log(`📋 Environment: ${SERVER.NODE_ENV}`);
console.log(`🎯 Loyalteez Brand ID: ${process.env.LOYALTEEZ_BRAND_ID || 'NOT SET'}`);
});
7. Testing with cURL
Track Single Event
curl -X POST http://localhost:3000/api/events/track \
-H "Content-Type: application/json" \
-d '{
"eventType": "account_creation",
"userEmail": "[email protected]",
"metadata": {
"source": "api_test",
"userId": "user_123"
}
}'
Track Batch Events
curl -X POST http://localhost:3000/api/events/batch \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"eventType": "account_creation",
"userEmail": "[email protected]"
},
{
"eventType": "newsletter_subscribe",
"userEmail": "[email protected]"
}
]
}'
Get Event Types
curl http://localhost:3000/api/events/types
Health Check
curl http://localhost:3000/health
8. Example: User Signup Flow
Complete user registration with automatic rewards:
const express = require('express');
const router = express.Router();
const loyalteez = require('../services/loyalteez');
const { EVENT_TYPES } = require('../config/constants');
/**
* User registration endpoint
*/
router.post('/register', async (req, res, next) => {
try {
const { email, password, name } = req.body;
// 1. Validate input
if (!email || !password || !name) {
return res.status(400).json({
success: false,
error: 'Missing required fields',
});
}
// 2. Create user in your database
const user = await createUserInDatabase({
email,
password, // Remember to hash this!
name,
});
// 3. Track account creation event (reward user with LTZ)
const rewardResult = await loyalteez.trackEvent(
EVENT_TYPES.ACCOUNT_CREATION,
email,
{
source: 'user_registration',
userId: user.id,
name: user.name,
}
);
// 4. Return success response
res.status(201).json({
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
},
reward: {
amount: rewardResult.rewardAmount,
walletAddress: rewardResult.walletAddress,
walletCreated: rewardResult.walletCreated,
},
message: `Welcome! You earned ${rewardResult.rewardAmount} LTZ tokens!`,
});
} catch (error) {
next(error);
}
});
// Mock database function (replace with your actual DB)
async function createUserInDatabase(userData) {
// Your user creation logic here
return {
id: 'user_' + Date.now(),
...userData,
createdAt: new Date(),
};
}
module.exports = router;
9. Example: Survey Completion
Reward users for completing surveys:
router.post('/surveys/:surveyId/complete', async (req, res, next) => {
try {
const { surveyId } = req.params;
const { userEmail, answers } = req.body;
// 1. Validate survey completion
const survey = await getSurveyById(surveyId);
if (!survey) {
return res.status(404).json({ error: 'Survey not found' });
}
// 2. Save survey responses
await saveSurveyResponses(surveyId, userEmail, answers);
// 3. Reward user with LTZ
const rewardResult = await loyalteez.trackEvent(
EVENT_TYPES.COMPLETE_SURVEY,
userEmail,
{
source: 'survey_completion',
surveyId,
answersCount: answers.length,
}
);
// 4. Return success
res.json({
success: true,
message: 'Survey completed successfully!',
reward: {
amount: rewardResult.rewardAmount,
eventId: rewardResult.eventId,
},
});
} catch (error) {
next(error);
}
});
10. Advanced: Retry Queue
Queue failed events for retry:
const Queue = require('bull');
const loyalteez = require('./services/loyalteez');
// Create queue
const eventQueue = new Queue('loyalteez-events', {
redis: {
host: 'localhost',
port: 6379,
},
});
// Process queue
eventQueue.process(async (job) => {
const { eventType, userEmail, metadata } = job.data;
try {
await loyalteez.trackEvent(eventType, userEmail, metadata);
console.log('✅ Queued event processed:', job.id);
} catch (error) {
console.error('❌ Queued event failed:', job.id, error.message);
throw error; // Will retry based on queue settings
}
});
// Add event to queue
async function queueEvent(eventType, userEmail, metadata) {
await eventQueue.add(
{ eventType, userEmail, metadata },
{
attempts: 5,
backoff: {
type: 'exponential',
delay: 2000,
},
}
);
}
module.exports = { queueEvent };
Best Practices
1. Error Handling
Always wrap Loyalteez calls in try-catch:
try {
await loyalteez.trackEvent(eventType, userEmail);
} catch (error) {
// Log error but don't break user flow
console.error('Reward failed, but continuing:', error);
// Maybe queue for retry
}
2. Idempotency
Track events with unique identifiers:
await loyalteez.trackEvent(eventType, userEmail, {
idempotencyKey: `${userEmail}-${eventType}-${Date.now()}`,
});
3. Rate Limiting
Respect Loyalteez rate limits (1 reward per event type per day per user).
4. Monitoring
Log all event tracking:
console.log('[LOYALTEEZ]', {
eventType,
userEmail,
success: true,
reward: result.rewardAmount,
});
Deployment
Environment Variables for Production
LOYALTEEZ_BRAND_ID=your_production_brand_id
LOYALTEEZ_API_URL=https://api.loyalteez.app
NODE_ENV=production
PORT=3000
Docker Support
Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src ./src
EXPOSE 3000
CMD ["node", "src/index.js"]
Troubleshooting
Events not tracking
- Verify
LOYALTEEZ_BRAND_IDis set - Check automation is enabled in Partner Portal
- Verify user email format is valid
Rate limit errors
- Implement client-side deduplication
- Cache user reward status
Timeout errors
- Increase axios timeout
- Implement retry logic with exponential backoff
Related Documentation
Need Help? Join our Discord or email [email protected]