REST API for Server-Side Reward Distribution
Complete reference for Loyalteez REST APIs - integrate from any backend language.
🎯 For Backend Developers: This guide covers server-side integration, batch operations, and custom implementations where the JavaScript SDK isn't suitable.
🚀 Try It Out: Interactive API Explorer → - Test endpoints directly in your browser!
Base URLs
Mainnet (Production)
| Service | Base URL | Purpose |
|---|---|---|
| Event Handler | https://api.loyalteez.app | Track events, reward users |
| Gas Relayer | https://relayer.loyalteez.app | Gasless transactions |
| Pregeneration | https://register.loyalteez.app | OAuth wallet creation |
Testnet (Testing)
| Service | Base URL | Purpose |
|---|---|---|
| Event Handler | https://api.loyalteez.xyz | Track events, reward users (testnet) |
| Gas Relayer | https://relayer.loyalteez.xyz | Gasless transactions (testnet) |
| Pregeneration | https://register.loyalteez.xyz | OAuth wallet creation (testnet) |
📚 Testnet Guide: See the Testnet Guide for complete testnet documentation, including contract addresses and Stripe sandbox information.
Authentication
Brand ID Authentication
Most endpoints require your Brand ID in the request body:
{
"brandId": "your-brand-id-here",
...
}
Where to find: Partner Portal → Settings → Account
Privy Token Authentication (Gas Relayer Only)
Gas Relayer requires a Privy access token:
Authorization: Bearer <privy_access_token>
How to get: Use Privy SDK's getAccessToken() method in your frontend.
Event Handler API
Base URL: https://api.loyalteez.app/loyalteez-api
POST /manual-event
Track a customer event and automatically reward them with LTZ.
Endpoint:
POST https://api.loyalteez.app/loyalteez-api/manual-event
Headers:
Content-Type: application/json
Request Body:
{
"brandId": "0x47511fc1c6664c9598974cb112965f8b198e0c725e",
"eventType": "account_creation",
"userEmail": "[email protected]",
"domain": "example.com",
"sourceUrl": "https://example.com/signup",
"metadata": {
"source": "backend_api",
"userId": "internal_user_123",
"customData": "any additional data"
}
}
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
brandId | string | Yes | Your brand wallet address (must be valid Ethereum address: 42 chars, starts with 0x). Must be lowercase (e.g., 0x47511fc1c6664c9598974cb112965f8b198e0c725e) |
eventType | string | Yes | Event type (alphanumeric + underscore only, max 50 chars) |
userEmail | string | Yes | User's email address (RFC 5322 compliant, max 254 chars) |
domain | string | Recommended | Your website domain (valid domain format, max 253 chars). If not provided, extracted from sourceUrl or Origin header |
sourceUrl | string | No | URL where event occurred (must be HTTP/HTTPS, max 2048 chars) |
userIdentifier | string | No | Alternative identifier (email or Ethereum address) |
metadata | object | No | Additional event data |
Important Notes:
- Brand ID Format: Must be lowercase Ethereum address. Convert using
brandId.toLowerCase()before sending. - Domain Authentication:
- The
domainfield must match your configured domain in Partner Portal → Settings → Domain Configuration - Domain is extracted from:
domainfield →sourceUrl→Originheader - Returns 403 if domain is not authorized for the brand
- Required: Add your domain to Partner Portal before making requests
- The
- CORS: For client-side requests, your domain must be whitelisted in the API CORS configuration. Contact support if you encounter CORS errors.
Supported Event Types & Default Rewards:
| Event Type | Default LTZ Reward | Description |
|---|---|---|
account_creation | 100 LTZ | User creates account |
complete_survey | 75 LTZ | User completes survey |
newsletter_subscribe | 25 LTZ | Newsletter signup |
rate_experience | 50 LTZ | User rates experience |
subscribe_renewal | 200 LTZ | Subscription renewal |
form_submit | 10 LTZ | Generic form submission |
Success Response (200):
{
"success": true,
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"message": "100 LTZ tokens minted successfully!",
"rewardAmount": 100,
"eventType": "account_creation",
"walletAddress": "0x1234...5678",
"transactionHash": "0xabcd...ef01",
"reward": {
"amount": 100,
"eventType": "account_creation",
"walletAddress": "0x1234...5678",
"transactionHash": "0xabcd...ef01"
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether event was recorded |
eventId | string | Unique event identifier |
message | string | Human-readable message |
rewardAmount | number | LTZ tokens to be distributed |
eventType | string | Event type processed |
walletCreated | boolean | Whether new wallet was created |
walletAddress | string | User's wallet address |
Error Response (400 - Invalid Data):
{
"error": "Invalid event data",
"details": [
"Missing required field: eventType",
"Invalid email format"
]
}
Error Response (403 - Domain Not Authorized):
{
"success": false,
"error": "Domain evil.com is not authorized for brand 0x47511fc1c6664c9598974cb112965f8b198e0c725e. Configured domain: example.com"
}
Error Response (409 - Duplicate Event):
{
"success": false,
"error": "Duplicate event detected. This event is already being processed.",
"details": {
"existingEventId": "550e8400-e29b-41d4-a716-446655440000"
}
}
Error Response (429 - Rate Limited):
{
"success": false,
"error": "Duplicate reward",
"details": {
"message": "You already received a reward for account_creation recently. Please wait 24 hours before trying again.",
"reason": "cooldown_not_expired",
"cooldownSeconds": 86400
}
}
Example Request (cURL):
curl -X POST https://api.loyalteez.app/loyalteez-api/manual-event \
-H "Content-Type: application/json" \
-d '{
"brandId": "your-brand-id",
"eventType": "account_creation",
"userEmail": "[email protected]",
"metadata": {
"source": "backend_api",
"plan": "premium"
}
}'
Example Request (Node.js):
const response = await fetch('https://api.loyalteez.app/loyalteez-api/manual-event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
brandId: process.env.LOYALTEEZ_BRAND_ID,
eventType: 'account_creation',
userEmail: '[email protected]',
metadata: {
source: 'backend_api',
userId: user.id
}
})
});
const data = await response.json();
console.log('Event tracked:', data);
Example Request (Python):
import requests
import os
response = requests.post(
'https://api.loyalteez.app/loyalteez-api/manual-event',
json={
'brandId': os.getenv('LOYALTEEZ_BRAND_ID'),
'eventType': 'account_creation',
'userEmail': '[email protected]',
'metadata': {
'source': 'backend_api',
'plan': 'premium'
}
}
)
data = response.json()
print(f"Event tracked: {data}")
POST /create-checkout
Create a Stripe checkout session for LTZ purchase.
Endpoint:
POST https://api.loyalteez.app/loyalteez-api/create-checkout
Request Body:
{
"brandId": "your-brand-id",
"amount": 1000,
"currency": "usd",
"userEmail": "[email protected]",
"successUrl": "https://yourdomain.com/success",
"cancelUrl": "https://yourdomain.com/cancel"
}
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
brandId | string | Yes | Your Brand ID |
amount | number | Yes | Amount in USD cents (1000 = $10.00) |
currency | string | No | Currency code (default: "usd") |
userEmail | string | Yes | User's email |
successUrl | string | Yes | Redirect after successful payment |
cancelUrl | string | Yes | Redirect if payment cancelled |
Success Response (200):
{
"success": true,
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_...",
"sessionId": "cs_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}
Example Request (Node.js):
const response = await fetch('https://api.loyalteez.app/loyalteez-api/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
brandId: 'your-brand-id',
amount: 5000, // $50.00
userEmail: '[email protected]',
successUrl: 'https://yoursite.com/success',
cancelUrl: 'https://yoursite.com/cancel'
})
});
const { checkoutUrl } = await response.json();
// Redirect user to checkoutUrl
window.location.href = checkoutUrl;
POST /stripe-mint
Internal webhook endpoint - handles Stripe payment completion and mints LTZ.
⚠️ Note: This endpoint is called automatically by Stripe webhooks. You don't need to call it directly.
GET /health
Health check endpoint - verify the Event Handler is operational.
Endpoint:
GET https://api.loyalteez.app/loyalteez-api/health
Success Response (200):
{
"status": "healthy",
"timestamp": "2025-11-11T14:30:00.000Z",
"services": {
"database": "connected",
"blockchain": "connected",
"privy": "connected"
}
}
Example Request:
curl https://api.loyalteez.app/loyalteez-api/health
GET /debug
Debug information about the worker configuration.
Endpoint:
GET https://api.loyalteez.app/loyalteez-api/debug
Success Response (200):
{
"debug": "API Worker debug info",
"hostname": "api.loyalteez.app",
"path": "/loyalteez-api/debug",
"method": "GET",
"timestamp": "2025-11-11T14:30:00.000Z",
"worker_version": "v2.0.0"
}
Gas Relayer API
Base URL: https://relayer.loyalteez.app
POST /relay
Execute a gasless blockchain transaction (user pays no gas fees).
Endpoint:
POST https://relayer.loyalteez.app/relay
Headers:
Content-Type: application/json
Authorization: Bearer <privy_access_token>
Request Body:
{
"to": "0xContractAddress...",
"data": "0xEncodedFunctionCall...",
"value": "0",
"gasLimit": 300000,
"userAddress": "0xUserWalletAddress...",
"metadata": {
"permit": {
"owner": "0xOwner...",
"spender": "0xSpender...",
"value": "1000000000000000000",
"deadline": 1699999999,
"v": 27,
"r": "0x...",
"s": "0x..."
}
}
}
Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Contract address to call |
data | string | Yes | Encoded function call data |
value | string | No | ETH value to send (default: "0") |
gasLimit | number | No | Gas limit (max: 1,000,000) |
userAddress | string | Yes | User's wallet address |
metadata.permit | object | No | EIP-2612 permit for gasless approval |
Whitelisted Contracts:
- Loyalteez Token (LTZ)
- PerkNFT Contract
- PointsSale Contract
- Business Escrow Contract
Rate Limits:
- 35 transactions per hour per user
- Resets every hour
Success Response (200):
{
"success": true,
"transactionHash": "0x1234abcd...",
"explorerUrl": "https://soneium.blockscout.com/tx/0x1234abcd...",
"gasUsed": "250000",
"effectiveGasPrice": "1500000000"
}
Error Response (401 - Unauthorized):
{
"error": "Invalid or expired Privy token"
}
Error Response (403 - Invalid Contract):
{
"error": "Transaction validation failed: Contract not whitelisted"
}
Error Response (429 - Rate Limit):
{
"error": "Rate limit exceeded. Max 35 transactions per hour."
}
Example Request (Frontend):
import { usePrivy } from '@privy-io/react-auth';
import { ethers } from 'ethers';
function MyComponent() {
const { getAccessToken, user } = usePrivy();
async function claimPerk() {
// 1. Get Privy access token
const token = await getAccessToken();
// 2. Encode contract call
const perkNFT = new ethers.Interface([
'function claimPerk(uint256 perkId)'
]);
const data = perkNFT.encodeFunctionData('claimPerk', [1]);
// 3. Call gas relayer
const response = await fetch('https://relayer.loyalteez.app/relay', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
to: process.env.VITE_PERK_NFT_ADDRESS,
data: data,
value: '0',
gasLimit: 300000,
userAddress: user.wallet.address
})
});
const result = await response.json();
console.log('Transaction hash:', result.transactionHash);
}
return <button onClick={claimPerk}>Claim Perk (Gasless)</button>;
}
Pregeneration API
Base URL: https://register.loyalteez.app
See Pregeneration API Reference for complete documentation.
Quick Reference:
POST https://register.loyalteez.app/loyalteez-api/pregenerate-user
{
"brand_id": "your-brand-id",
"oauth_provider": "discord",
"oauth_user_id": "123456789012345678",
"oauth_username": "user123"
}
Rate Limits
| Endpoint | Limit | Scope | Reset |
|---|---|---|---|
/manual-event | Based on event rule | Per user email | Cooldown-based (default: 24 hours) |
/manual-event | Duplicate detection | Same event + user | 60 seconds (returns 409) |
/relay | 35 transactions | Per user wallet | Hourly |
/pregenerate-user | 100 requests | Per brand | Per minute |
Event Rate Limiting Details:
- Duplicate Detection: Same event type + user email within last 60 seconds → 409 Conflict
- Cooldown: Based on
cooldownHoursin event rule (default: 24 hours) → 429 Too Many Requests - Max Claims: Based on
maxClaimsin event rule (default: 1) → 429 Too Many Requests
Rate Limit Headers:
When rate limited, responses include:
X-RateLimit-Limit: 35
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699999999
Error Codes
| Status Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid data format, validation errors (invalid email, brandId format, etc.) |
| 401 | Unauthorized | Invalid/missing Privy token (Gas Relayer only) |
| 403 | Forbidden | Domain not authorized for brand, automation disabled |
| 404 | Not Found | Invalid endpoint |
| 409 | Conflict | Duplicate event detected (same event + user within 60 seconds) |
| 413 | Payload Too Large | Request body exceeds 10KB limit |
| 429 | Too Many Requests | Rate limit exceeded, cooldown period not expired |
| 500 | Internal Server Error | Server error, blockchain transaction failed |
| 503 | Service Unavailable | Database not configured |
Standard Error Response Format:
{
"error": "Error type",
"message": "Human-readable error description",
"details": "Additional error context (optional)"
}
CORS
CORS headers are set based on brand-domain authentication:
- Authorized Domains: CORS headers set to the authorized domain (from brand configuration)
- Default Origins: Loyalteez domains (loyalteez.app, api.loyalteez.app, etc.) are always allowed
- Subdomain Support:
subdomain.example.commatchesexample.comconfiguration
Headers:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Preflight Requests:
All endpoints automatically handle OPTIONS requests for CORS preflight.
Note: CORS is NOT wildcard (*). Headers are set based on the authorized domain for the brand.
Best Practices
1. Error Handling
Always handle errors gracefully:
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.ok) {
const error = await response.json();
console.error('Event tracking failed:', error);
// Handle specific errors
if (response.status === 429) {
// Rate limited - try again later
} else if (response.status === 403) {
// Automation disabled - notify user
}
return;
}
const data = await response.json();
console.log('Event tracked successfully:', data);
} catch (error) {
console.error('Network error:', error);
// Queue for retry
}
2. Idempotency
Some endpoints are idempotent (safe to retry):
- ✅
/pregenerate-user- Returns same wallet for same OAuth ID - ⚠️
/manual-event- Has daily deduplication per event type - ⚠️
/relay- Each transaction is unique
3. Environment Variables
Store configuration securely:
# .env
LOYALTEEZ_BRAND_ID=your_brand_id
LOYALTEEZ_API_URL=https://api.loyalteez.app
4. Logging
Log all API calls for debugging:
const logAPICall = async (endpoint, method, body) => {
console.log(`[Loyalteez API] ${method} ${endpoint}`, {
timestamp: new Date().toISOString(),
body: body
});
};
Testing
Test Mode
Use these test emails to avoid affecting production:
const testEmails = [
'[email protected]',
'[email protected]'
];
Debug Mode
Enable debug logging:
const DEBUG = process.env.NODE_ENV === 'development';
if (DEBUG) {
console.log('Request:', requestBody);
console.log('Response:', responseData);
}
SDK vs REST API
| Use Case | Recommended |
|---|---|
| Frontend web apps | JavaScript SDK |
| Backend systems | REST API |
| Mobile apps (React Native) | JavaScript SDK |
| Mobile apps (Native) | REST API |
| Batch operations | REST API |
| Server-side tracking | REST API |
| Gasless transactions | Both (SDK easier) |
Support
Need Help?
- 📖 Complete API Documentation
- 💬 Discord Community
- 📧 Email: [email protected]
Found a Bug?
- 🐛 Report: [email protected]
- Include: Request/response, timestamp, error message
Related Documentation
- Event Handler API - Detailed event tracking guide
- Gas Relayer API - Gasless transactions guide
- Pregeneration API - OAuth wallet creation guide
- JavaScript SDK - Frontend SDK documentation
- Error Codes - Complete error reference
API Version: v2.0