Skip to main content

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)

ServiceBase URLPurpose
Event Handlerhttps://api.loyalteez.appTrack events, reward users
Gas Relayerhttps://relayer.loyalteez.appGasless transactions
Pregenerationhttps://register.loyalteez.appOAuth wallet creation

Testnet (Testing)

ServiceBase URLPurpose
Event Handlerhttps://api.loyalteez.xyzTrack events, reward users (testnet)
Gas Relayerhttps://relayer.loyalteez.xyzGasless transactions (testnet)
Pregenerationhttps://register.loyalteez.xyzOAuth 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:

FieldTypeRequiredDescription
brandIdstringYesYour brand wallet address (must be valid Ethereum address: 42 chars, starts with 0x). Must be lowercase (e.g., 0x47511fc1c6664c9598974cb112965f8b198e0c725e)
eventTypestringYesEvent type (alphanumeric + underscore only, max 50 chars)
userEmailstringYesUser's email address (RFC 5322 compliant, max 254 chars)
domainstringRecommendedYour website domain (valid domain format, max 253 chars). If not provided, extracted from sourceUrl or Origin header
sourceUrlstringNoURL where event occurred (must be HTTP/HTTPS, max 2048 chars)
userIdentifierstringNoAlternative identifier (email or Ethereum address)
metadataobjectNoAdditional event data

Important Notes:

  1. Brand ID Format: Must be lowercase Ethereum address. Convert using brandId.toLowerCase() before sending.
  2. Domain Authentication:
    • The domain field must match your configured domain in Partner Portal → Settings → Domain Configuration
    • Domain is extracted from: domain field → sourceUrlOrigin header
    • Returns 403 if domain is not authorized for the brand
    • Required: Add your domain to Partner Portal before making requests
  3. 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 TypeDefault LTZ RewardDescription
account_creation100 LTZUser creates account
complete_survey75 LTZUser completes survey
newsletter_subscribe25 LTZNewsletter signup
rate_experience50 LTZUser rates experience
subscribe_renewal200 LTZSubscription renewal
form_submit10 LTZGeneric 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:

FieldTypeDescription
successbooleanWhether event was recorded
eventIdstringUnique event identifier
messagestringHuman-readable message
rewardAmountnumberLTZ tokens to be distributed
eventTypestringEvent type processed
walletCreatedbooleanWhether new wallet was created
walletAddressstringUser'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:

FieldTypeRequiredDescription
brandIdstringYesYour Brand ID
amountnumberYesAmount in USD cents (1000 = $10.00)
currencystringNoCurrency code (default: "usd")
userEmailstringYesUser's email
successUrlstringYesRedirect after successful payment
cancelUrlstringYesRedirect 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:

FieldTypeRequiredDescription
tostringYesContract address to call
datastringYesEncoded function call data
valuestringNoETH value to send (default: "0")
gasLimitnumberNoGas limit (max: 1,000,000)
userAddressstringYesUser's wallet address
metadata.permitobjectNoEIP-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

EndpointLimitScopeReset
/manual-eventBased on event rulePer user emailCooldown-based (default: 24 hours)
/manual-eventDuplicate detectionSame event + user60 seconds (returns 409)
/relay35 transactionsPer user walletHourly
/pregenerate-user100 requestsPer brandPer minute

Event Rate Limiting Details:

  • Duplicate Detection: Same event type + user email within last 60 seconds → 409 Conflict
  • Cooldown: Based on cooldownHours in event rule (default: 24 hours) → 429 Too Many Requests
  • Max Claims: Based on maxClaims in 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 CodeMeaningCommon Causes
400Bad RequestMissing required fields, invalid data format, validation errors (invalid email, brandId format, etc.)
401UnauthorizedInvalid/missing Privy token (Gas Relayer only)
403ForbiddenDomain not authorized for brand, automation disabled
404Not FoundInvalid endpoint
409ConflictDuplicate event detected (same event + user within 60 seconds)
413Payload Too LargeRequest body exceeds 10KB limit
429Too Many RequestsRate limit exceeded, cooldown period not expired
500Internal Server ErrorServer error, blockchain transaction failed
503Service UnavailableDatabase 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.com matches example.com configuration

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 CaseRecommended
Frontend web appsJavaScript SDK
Backend systemsREST API
Mobile apps (React Native)JavaScript SDK
Mobile apps (Native)REST API
Batch operationsREST API
Server-side trackingREST API
Gasless transactionsBoth (SDK easier)

Support

Need Help?

Found a Bug?



API Version: v2.0