useLoyalty
Server SDK

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/events

Request Body

{
  "externalId": "user_123",
  "event": "product_review",
  "properties": {
    "productId": "prod_456",
    "rating": 5,
    "hasPhoto": true
  }
}

Parameters

FieldTypeRequiredDescription
externalIdstringYesMember's external ID
eventstringYesEvent name (matches quest action field)
propertiesobjectNoAdditional event metadata
timestampstringNoISO 8601 override (default: now)
idempotencyKeystringNoUnique 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
}
FieldDescription
questsTriggeredIDs of quests that were triggered by this event
pointsAwardedTotal points awarded across all triggered quests
badgesAwardedBadge IDs unlocked as a result of this event
streakUpdatedtrue if the event extended or started a streak
tierChangedtrue if the member moved to a new tier
newTierTier 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

  1. Event Received: Your app sends an event (e.g., product_review)
  2. Quest Matching: System finds all AUTOMATED quests with matching action
  3. Validation: Each quest checks limits, cooldowns, and time windows
  4. Progress Update: Quest progress increments or completes
  5. Points Award: Completed quests award points (with multipliers)
  6. Response: Returns all triggered and completed quests
Your App → trackEvent("purchase") → Finds matching quests → Awards points

Event Naming Conventions

Use consistent, descriptive event names:

EventDescription
signupUser creates account
loginUser logs in
profile_completeUser completes profile
purchaseUser makes a purchase
product_reviewUser writes a review
referral_sentUser sends referral
social_shareUser shares on social media
newsletter_subscribeUser subscribes to newsletter
app_installUser 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 completed

Event 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 });
});

On this page