@loyalty/node is the official Node.js SDK. It wraps every endpoint with full TypeScript types, automatic error handling, and built-in webhook signature verification — zero dependencies, native fetch.
npm install @loyalty/node
# or: pnpm add @loyalty/node · yarn add @loyalty/nodeRequires Node.js 18+ (for native fetch). Ships with its own type declarations — no @types package needed.
Create one client and reuse it. Keep your apiKey in an environment variable — never hard-code it or expose it to the browser.
import { LoyaltyClient } from '@loyalty/node';
const loyalty = new LoyaltyClient({
tenantId: process.env.LOYALTY_TENANT_ID!,
apiKey: process.env.LOYALTY_API_KEY!, // sk_live_* or sk_test_*
// baseUrl is optional — defaults to the production engine
});evaluate() runs the basket against all active campaigns. It awards points and issues any tangible rewards (coupons, gift cards, free items, discount vouchers) in a single call. Each matched campaign returns a rewardIds array.
// Award points and/or issue rewards for a basket — auto-creates the member
const result = await loyalty.evaluate({
transactionRef: 'receipt_001',
memberRef: 'customer_27821234567',
amount: 15000, // always in cents
});
console.log(result.pointsAwarded); // 150
console.log(result.pointsBalance); // 150
console.log(result.campaigns[0].rewardIds); // ['rwd_...'] when a reward firedPreview a discount with redeemQuote() (no writes), then commit it with redeem(). Both evaluate() and redeem() are idempotent on transactionRef.
// Preview a redemption — no database writes
const quote = await loyalty.redeemQuote({
memberRef: 'customer_27821234567',
amount: 15000,
pointsRequested: 100,
});
console.log(quote.discountAmount); // 100 (cents)
// Commit the redemption — idempotent on transactionRef
const redeemed = await loyalty.redeem({
transactionRef: 'redeem_001',
memberRef: 'customer_27821234567',
amount: 15000,
pointsRequested: 100,
});
console.log(redeemed.balanceAfter);Every delivery is signed with HMAC-SHA256 in the X-Loyalty-Signature header. The SDK ships a constant-time verifyWebhookSignature() helper — always verify against the raw request body before processing.
import { verifyWebhookSignature } from '@loyalty/node';
import express from 'express';
const app = express();
app.post('/webhooks/loyalty', express.text({ type: '*/*' }), (req, res) => {
const valid = verifyWebhookSignature(
req.body, // raw string body
req.headers['x-loyalty-signature'] as string,
process.env.LOYALTY_WEBHOOK_SECRET!,
);
if (!valid) return res.status(401).end();
const event = JSON.parse(req.body);
switch (event.event) {
case 'transaction.processed': /* … */ break;
case 'reward.issued': /* … */ break;
}
res.status(200).json({ received: true });
});Failed requests throw a typed LoyaltyError carrying the engine's code, HTTP status, and a human-readable detail. See the error reference for the full code list.
import { LoyaltyError } from '@loyalty/node';
try {
await loyalty.redeem({ /* … */ });
} catch (err) {
if (err instanceof LoyaltyError) {
console.error(err.code); // e.g. 'INSUFFICIENT_POINTS'
console.error(err.status); // e.g. 422
console.error(err.detail); // human-readable explanation
} else {
throw err; // network or unexpected error
}
}sk_test_* keys — they hit an isolated environment where no real transactions occur. See the testing guide.