useLoyalty
Server SDK

Points

Award, deduct, and track points for your members

Points are the core currency in your gamification system. Use the Points API to award points for actions, deduct for redemptions, and track transaction history.

Get Points Balance

Retrieve a member's current points balance and optionally their transaction history.

GET /api/v1/members/:externalId/points

Query Parameters

ParameterTypeDefaultDescription
historybooleanfalseInclude transaction history
limitnumber20Number of transactions to return

Example

curl -X GET "https://app.useloyalty.app/api/v1/members/user_123/points?history=true&limit=10" \
  -H "Authorization: Bearer sk_your_key"

Response

{
  "points": 1500,
  "totalPointsEarned": 2500,
  "history": [
    {
      "id": "txn_abc123",
      "amount": 100,
      "balance": 1500,
      "type": "EARNED",
      "description": "Completed daily quest",
      "referenceId": "quest_daily_checkin",
      "createdAt": "2024-01-20T10:30:00Z"
    },
    {
      "id": "txn_def456",
      "amount": -500,
      "balance": 1400,
      "type": "REDEEMED",
      "description": "Claimed 10% discount reward",
      "referenceId": "reward_10_discount",
      "createdAt": "2024-01-19T15:00:00Z"
    }
  ]
}

Add or Deduct Points

Award points to a member or deduct points (use negative amount).

POST /api/v1/members/:externalId/points

Request Body

{
  "amount": 100,
  "description": "Completed profile setup"
}

Parameters

FieldTypeRequiredDescription
amountnumberYesPoints to add (positive) or deduct (negative)
descriptionstringNoHuman-readable description
sourcestringNoOriginating system tag (e.g. shopify, discord)
expiresAtstringNoISO 8601 — set expiry on these specific points
idempotencyKeystringNoUnique key to safely retry without double-awarding
referenceIdstringNoLink to related entity (quest, order, etc.)
metadataobjectNoAdditional context data

Always provide an idempotencyKey for point mutations. If a request times out and you retry, the server will deduplicate using this key and return the original result instead of awarding twice.

SDK

// Award with idempotency
await useLoyalty.members.awardPoints("user_123", {
  amount: 100,
  description: "Order #5521 bonus",
  source: "shopify",
  idempotencyKey: `order-5521-points`,
});

// Deduct with idempotency
await useLoyalty.members.deductPoints("user_123", {
  amount: 500,
  description: "Redeemed discount reward",
  idempotencyKey: `redemption-${redemptionId}`,
});

Response

{
  "transaction": {
    "id": "txn_xyz789",
    "amount": 100,
    "balance": 1600,
    "type": "EARNED",
    "description": "Completed profile setup",
    "createdAt": "2024-01-20T11:00:00Z"
  },
  "member": {
    "id": "clx123abc",
    "externalId": "user_123",
    "points": 1600,
    "totalPointsEarned": 2600
  }
}

Transaction Types

TypeDescription
EARNEDPoints awarded for actions (positive)
REDEEMEDPoints spent on rewards (negative)
ADJUSTEDManual adjustment by admin
EXPIREDPoints removed due to expiration
REFUNDEDPoints returned (e.g., order refund)
REFERRAL_BONUSPoints from referral program

Points Multipliers

When a points multiplier event is active, awarded points are automatically multiplied:

{
  "amount": 100,
  "description": "Daily checkin",
  "metadata": {
    "basePoints": 100,
    "multiplier": 2.0,
    "multiplierEventName": "Double Points Weekend"
  }
}

The response includes metadata showing the multiplier applied.

Code Examples

Award Points for Action

async function awardPoints(userId: string, amount: number, reason: string) {
  const response = await fetch(
    `https://app.useloyalty.app/api/v1/members/${userId}/points`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.USELOYALTY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount,
        description: reason,
      }),
    },
  );

  return response.json();
}

// Usage
await awardPoints("user_123", 50, "Wrote a product review");
await awardPoints("user_123", 100, "Completed onboarding");

Deduct Points for Redemption

async function deductPoints(userId: string, amount: number, rewardId: string) {
  const response = await fetch(
    `https://app.useloyalty.app/api/v1/members/${userId}/points`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.USELOYALTY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount: -amount, // Negative for deduction
        description: `Redeemed reward`,
        referenceId: rewardId,
      }),
    },
  );

  return response.json();
}

Check Balance Before Redemption

async function canAffordReward(userId: string, cost: number): Promise<boolean> {
  const response = await fetch(
    `https://app.useloyalty.app/api/v1/members/${userId}/points`,
    {
      headers: {
        Authorization: `Bearer ${process.env.USELOYALTY_API_KEY}`,
      },
    },
  );

  const data = await response.json();
  return data.points >= cost;
}

Get Transaction History

GET /api/v1/members/:externalId/transactions
// SDK
const { data, hasMore, nextCursor } = await useLoyalty.members.getTransactions(
  "user_123",
  {
    limit: 20,
    cursor: previousCursor,
  },
);

// Each transaction:
// {
//   id: "txn_abc",
//   type: "AWARD" | "DEDUCT" | "EXPIRY" | "REDEMPTION",
//   amount: 100,
//   balance: 1600,
//   description: "Order bonus",
//   source: "shopify",
//   expiresAt: null,
//   createdAt: "2024-01-20T10:30:00Z"
// }

Bulk Points Operations

For high-volume scenarios, batch your point awards:

async function awardBulkPoints(
  awards: Array<{ userId: string; amount: number; reason: string }>,
) {
  // Process in parallel with rate limiting
  const batchSize = 10;

  for (let i = 0; i < awards.length; i += batchSize) {
    const batch = awards.slice(i, i + batchSize);
    await Promise.all(
      batch.map((award) =>
        awardPoints(award.userId, award.amount, award.reason),
      ),
    );

    // Rate limit: wait 100ms between batches
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
}

Best Practices

1. Use Descriptive Messages

// Good
await awardPoints(userId, 50, "Completed 5th purchase milestone");

// Avoid
await awardPoints(userId, 50, "points");

2. Include Reference IDs

Link transactions to related entities for auditing:

await awardPoints(userId, 100, "Order bonus", {
  referenceId: `order_${orderId}`,
  metadata: {
    orderTotal: 150.0,
    orderDate: new Date().toISOString(),
  },
});

3. Handle Insufficient Balance

async function redeemReward(userId: string, reward: Reward) {
  const balance = await getPointsBalance(userId);

  if (balance < reward.cost) {
    throw new Error(
      `Insufficient points. Need ${reward.cost}, have ${balance}`,
    );
  }

  await deductPoints(userId, reward.cost, reward.id);
}

4. Use Events for Automated Awards

For recurring actions, use the Events API instead of direct points:

// Instead of:
await awardPoints(userId, 10, "Daily login");

// Use events (handles deduplication, cooldowns, limits):
await trackEvent(userId, "daily_login");

On this page