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/pointsQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
history | boolean | false | Include transaction history |
limit | number | 20 | Number 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/pointsRequest Body
{
"amount": 100,
"description": "Completed profile setup"
}Parameters
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Points to add (positive) or deduct (negative) |
description | string | No | Human-readable description |
source | string | No | Originating system tag (e.g. shopify, discord) |
expiresAt | string | No | ISO 8601 — set expiry on these specific points |
idempotencyKey | string | No | Unique key to safely retry without double-awarding |
referenceId | string | No | Link to related entity (quest, order, etc.) |
metadata | object | No | Additional 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
| Type | Description |
|---|---|
EARNED | Points awarded for actions (positive) |
REDEEMED | Points spent on rewards (negative) |
ADJUSTED | Manual adjustment by admin |
EXPIRED | Points removed due to expiration |
REFUNDED | Points returned (e.g., order refund) |
REFERRAL_BONUS | Points 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");