Events
Track custom events that trigger automated quest completions
The Events API lets you track user actions that automatically trigger quest completions. This is the recommended way to integrate gamification into your application.
Track Event
Send a custom event that matches against AUTOMATED quests.
POST /api/v1/eventsRequest Body
{
"externalId": "user_123",
"event": "product_review",
"properties": {
"productId": "prod_456",
"rating": 5,
"hasPhoto": true
}
}Parameters
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Yes | Member's external ID |
event | string | Yes | Event name (matches quest action field) |
properties | object | No | Additional event metadata |
timestamp | string | No | ISO 8601 override (default: now) |
idempotencyKey | string | No | Unique key to safely retry without double-triggering |
Provide an idempotencyKey for any event that could be retried (e.g. webhook
redelivery, network timeout). The server deduplicates and returns the original
result.
SDK
// Full event
await useLoyalty.events.track("user_123", {
event: "product_review",
properties: { productId: "prod_456", rating: 5 },
idempotencyKey: `review-${reviewId}`,
});
// Convenience shorthand
await useLoyalty.events.trackSimple("user_123", "daily_login");
await useLoyalty.events.trackSimple("user_123", "purchase", { amount: 49.99 });Response
{
"eventId": "evt_abc123",
"questsTriggered": ["quest_first_review"],
"pointsAwarded": 100,
"badgesAwarded": ["badge_reviewer"],
"streakUpdated": true,
"tierChanged": false,
"newTier": null
}| Field | Description |
|---|---|
questsTriggered | IDs of quests that were triggered by this event |
pointsAwarded | Total points awarded across all triggered quests |
badgesAwarded | Badge IDs unlocked as a result of this event |
streakUpdated | true if the event extended or started a streak |
tierChanged | true if the member moved to a new tier |
newTier | Tier name if tierChanged is true, otherwise null |
Legacy response shape (pre-SDK)
{
"tracked": true,
"event": "product_review",
"externalId": "user_123",
"questsTriggered": 2,
"questsCompleted": [
{
"id": "comp_abc123",
"questId": "quest_first_review",
"questName": "Write Your First Review",
"pointsAwarded": 100,
"basePoints": 100,
"multiplier": 1,
"multiplierEventName": null,
"completedAt": "2024-01-20T15:00:00Z"
},
{
"id": "comp_def456",
"questId": "quest_photo_review",
"questName": "Review with Photo",
"pointsAwarded": 200,
"basePoints": 100,
"multiplier": 2,
"multiplierEventName": "Double Points Week",
"completedAt": "2024-01-20T15:00:00Z"
}
]
}How It Works
- Event Received: Your app sends an event (e.g.,
product_review) - Quest Matching: System finds all AUTOMATED quests with matching
action - Validation: Each quest checks limits, cooldowns, and time windows
- Progress Update: Quest progress increments or completes
- Points Award: Completed quests award points (with multipliers)
- Response: Returns all triggered and completed quests
Your App → trackEvent("purchase") → Finds matching quests → Awards pointsEvent Naming Conventions
Use consistent, descriptive event names:
| Event | Description |
|---|---|
signup | User creates account |
login | User logs in |
profile_complete | User completes profile |
purchase | User makes a purchase |
product_review | User writes a review |
referral_sent | User sends referral |
social_share | User shares on social media |
newsletter_subscribe | User subscribes to newsletter |
app_install | User installs mobile app |
Code Examples
Basic Event Tracking
const USELOYALTY_API_KEY = process.env.USELOYALTY_API_KEY;
async function trackEvent(
userId: string,
event: string,
properties?: Record<string, any>,
) {
const response = await fetch("https://app.useloyalty.app/api/v1/events", {
method: "POST",
headers: {
Authorization: `Bearer ${USELOYALTY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
externalId: userId,
event,
properties,
}),
});
return response.json();
}Track Purchase Event
async function onPurchaseComplete(order: Order) {
const result = await trackEvent(order.userId, "purchase", {
orderId: order.id,
total: order.total,
itemCount: order.items.length,
categories: order.items.map((i) => i.category),
});
// Show achievement notification if quests completed
if (result.questsCompleted.length > 0) {
for (const quest of result.questsCompleted) {
showNotification({
title: "Quest Completed!",
message: `${quest.questName} - Earned ${quest.pointsAwarded} points`,
});
}
}
}Track Multiple Events
async function onReviewSubmit(review: Review) {
// Track base review event
await trackEvent(review.userId, "product_review", {
productId: review.productId,
rating: review.rating,
});
// Track additional events based on review quality
if (review.hasPhoto) {
await trackEvent(review.userId, "photo_review");
}
if (review.text.length > 200) {
await trackEvent(review.userId, "detailed_review");
}
if (review.rating === 5) {
await trackEvent(review.userId, "five_star_review");
}
}Integration with Express.js
import express from "express";
const app = express();
// Middleware to track events
app.post("/api/reviews", async (req, res) => {
const { userId, productId, rating, text } = req.body;
// Save review to database
const review = await createReview({ userId, productId, rating, text });
// Track gamification event (fire and forget)
trackEvent(userId, "product_review", {
productId,
rating,
reviewLength: text.length,
}).catch(console.error);
res.json(review);
});Integration with Next.js API Routes
// pages/api/purchase/complete.ts
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { orderId, userId } = req.body;
// Process order...
const order = await processOrder(orderId);
// Track purchase event
const useLoyaltyResult = await trackEvent(userId, "purchase", {
orderId: order.id,
total: order.total,
});
res.json({
order,
pointsEarned: useLoyaltyResult.questsCompleted.reduce(
(sum, q) => sum + q.pointsAwarded,
0,
),
});
}Auto-Creating Members
If a member doesn't exist when an event is tracked, they are automatically created:
// This works even if user_new hasn't been created yet
await trackEvent("user_new", "signup");
// Member is created and quest is completedEvent Deduplication
Events are processed based on quest cooldowns and limits. The same event can be sent multiple times safely:
// First call - completes quest
await trackEvent("user_123", "daily_login");
// { questsCompleted: [{ questName: "Daily Login", ... }] }
// Second call same day - no duplicate completion
await trackEvent("user_123", "daily_login");
// { questsTriggered: 1, questsCompleted: [] }Error Handling
async function safeTrackEvent(userId: string, event: string, props?: any) {
try {
const result = await trackEvent(userId, event, props);
return result;
} catch (error) {
// Log error but don't fail the main operation
console.error("Gamification event tracking failed:", error);
// Return empty result to continue app flow
return {
tracked: false,
questsTriggered: 0,
questsCompleted: [],
};
}
}Best Practices
1. Track Events Asynchronously
Don't block user operations waiting for gamification:
// Good - fire and forget
createOrder(order).then(() => {
trackEvent(order.userId, "purchase").catch(console.error);
});
// Avoid - blocking
await createOrder(order);
await trackEvent(order.userId, "purchase");2. Use Specific Event Names
// Good - specific and actionable
await trackEvent(userId, "first_purchase");
await trackEvent(userId, "repeat_purchase");
await trackEvent(userId, "high_value_purchase");
// Avoid - too generic
await trackEvent(userId, "action");3. Include Useful Properties
await trackEvent(userId, "purchase", {
orderId: order.id,
total: order.total,
currency: "USD",
itemCount: order.items.length,
isFirstPurchase: customer.orderCount === 1,
paymentMethod: order.payment.method,
});4. Handle Webhook Integrations
For events from external systems:
// Stripe webhook handler
app.post("/webhooks/stripe", async (req, res) => {
const event = req.body;
if (event.type === "payment_intent.succeeded") {
const userId = event.data.object.metadata.userId;
await trackEvent(userId, "purchase", {
stripePaymentId: event.data.object.id,
amount: event.data.object.amount / 100,
});
}
res.json({ received: true });
});