Libraries and SDKs
The official Loyalino Node SDK is a single, dependency-free file that wraps the REST API and webhook signature verification. It runs on Node 18 and later.
Install
Download the SDK and drop it into your project, or copy the source from the bottom of this page.
curl -o loyalino.js https://help.loyalino.io/sdk.jsfetch and node:crypto, so there is nothing to install.Quickstart
import { randomUUID } from "node:crypto";
import Loyalino from "./loyalino.js";
const loyalino = new Loyalino(process.env.LOYALINO_API_KEY);
// Read your program settings
const program = await loyalino.getProgram();
// Find a customer and award points
const { data } = await loyalino.listCustomers({ email: "ada@example.com" });
const customer = data[0];
await loyalino.awardPoints(customer.id, 100, {
type: "earned",
description: "Birthday bonus",
idempotency_key: "birthday-" + customer.id + "-2026",
});
// Redeem a reward. Store the key first and reuse it if you retry.
const key = randomUUID();
const redemption = await loyalino.redeem(customer.id, { reward_id: "rw_5" }, key);
console.log(redemption.discount_code);
// What the member sees on your storefront, for your own app
const member = await loyalino.getMember(customer.id, { locale: "da", country: "DK" });
// Current native Shopify store credit, kept separate for each currency.
const { balances } = await loyalino.getStoreCredit(customer.id);
await loyalino.completeMemberAction(customer.id, { action_type: "newsletter" });Methods
| Method | Calls |
|---|---|
listCustomers(query) | GET /customers |
getCustomer(id) | GET /customers/:id |
createCustomer(body) | POST /customers |
updateCustomer(id, body) | PATCH /customers/:id |
getMember(id, query) | GET /customers/:id/member |
getStoreCredit(id) | GET /customers/:id/store_credit |
listScratchCardClaims(id,{cursor}) | GET /customers/:id/scratch_cards |
previewScratchCard(id,code) | POST /customers/:id/scratch_cards/preview |
claimScratchCard(id,code,idempotencyKey) | POST /customers/:id/scratch_cards/claim |
completeMemberAction(id, body) | POST /customers/:id/member_actions |
awardPoints(id, points, extra) | POST /customers/:id/points_transactions |
listCustomerTransactions(id, query) | GET /customers/:id/points_transactions |
listTransactions(query) | GET /points_transactions |
getTransaction(id) | GET /points_transactions/:id |
createActivity(body) | POST /activities |
redeem(customerId, body, idempotencyKey) | POST /customers/:id/redemptions |
listRewards(query) | GET /rewards |
getReward(id) | GET /rewards/:id |
listEarnRules(query) | GET /earn_rules |
listVipTiers(query) | GET /vip_tiers |
getProgram() | GET /program |
listWebhookEndpoints(query) | GET /webhook_endpoints |
createWebhookEndpoint(body) | POST /webhook_endpoints |
deleteWebhookEndpoint(id) | DELETE /webhook_endpoints/:id |
Every method returns the parsed JSON response. On a non-2xx status the SDK throws an Error with .status and .code set from the error envelope.
getStoreCredit reads native Shopify credit and returns {balances: [{amount, currencyCode}]}. Amounts remain decimal strings and currencies stay separate. A failed read throws; do not turn it into a displayed zero. Spending credit in checkout requires Shopify new customer account authentication.
Verify webhooks
Use the exported verifyWebhook helper with your endpoint secret, the raw request body, and the Loyalino-Signature header.
import { verifyWebhook } from "./loyalino.js";
// Express example (use the raw body, not the parsed JSON)
app.post("/loyalino-webhook", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyWebhook(
process.env.LOYALINO_WEBHOOK_SECRET,
req.headers["loyalino-signature"],
req.body.toString("utf8"),
);
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.type, event.data);
res.sendStatus(200);
});Full source
The entire SDK. Copy it into loyalino.js if you prefer not to download it.
// loyalino.js — official Node client for the Loyalino loyalty REST API.
// Requires Node 18+ (uses global fetch). ESM. No dependencies.
//
// import Loyalino, { verifyWebhook } from "./loyalino.js";
// const loyalino = new Loyalino(process.env.LOYALINO_API_KEY);
// const program = await loyalino.getProgram();
//
import crypto from "node:crypto";
export class Loyalino {
constructor(apiKey, options) {
if (!apiKey) throw new Error("Loyalino: apiKey is required");
options = options || {};
let base = options.baseUrl || "https://app.loyalino.io/api/v1";
while (base.length && base.charAt(base.length - 1) === "/") base = base.slice(0, -1);
this.apiKey = apiKey;
this.baseUrl = base;
}
async request(method, path, opts) {
opts = opts || {};
const url = new URL(this.baseUrl + path);
if (opts.query) {
for (const key of Object.keys(opts.query)) {
const value = opts.query[key];
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
}
}
const headers = { Authorization: "Bearer " + this.apiKey };
if (opts.body) headers["Content-Type"] = "application/json";
if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
const res = await fetch(url, {
method: method,
headers: headers,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
const text = await res.text();
const data = text ? JSON.parse(text) : {};
if (!res.ok) {
const err = new Error((data.error && data.error.message) || ("Loyalino API error " + res.status));
err.status = res.status;
err.code = data.error && data.error.code;
throw err;
}
return data;
}
// Customers
listCustomers(query) { return this.request("GET", "/customers", { query: query }); }
getCustomer(id) { return this.request("GET", "/customers/" + encodeURIComponent(id)); }
createCustomer(body) { return this.request("POST", "/customers", { body: body }); }
updateCustomer(id, body) { return this.request("PATCH", "/customers/" + encodeURIComponent(id), { body: body }); }
// Member view + actions (what a signed-in member sees and does on the storefront)
getMember(id, query) { return this.request("GET", "/customers/" + encodeURIComponent(id) + "/member", { query: query }); }
getStoreCredit(id) { return this.request("GET", "/customers/" + encodeURIComponent(id) + "/store_credit"); }
listScratchCardClaims(id, query) { return this.request("GET", "/customers/" + encodeURIComponent(id) + "/scratch_cards", { query: query }); }
// Codes stay in POST bodies. Keep one caller key for ambiguous retries;
// the immutable physical card is the server's once-only economic identity.
previewScratchCard(id, code) {
return this.request("POST", "/customers/" + encodeURIComponent(id) + "/scratch_cards/preview", { body: { code: code } });
}
claimScratchCard(id, code, idempotencyKey) {
return this.request("POST", "/customers/" + encodeURIComponent(id) + "/scratch_cards/claim", { body: { code: code }, idempotencyKey: idempotencyKey });
}
completeMemberAction(id, body) {
return this.request("POST", "/customers/" + encodeURIComponent(id) + "/member_actions", { body: body });
}
// Points
awardPoints(id, points, extra) {
const body = Object.assign({ points: points }, extra || {});
return this.request("POST", "/customers/" + encodeURIComponent(id) + "/points_transactions", { body: body });
}
listCustomerTransactions(id, query) {
return this.request("GET", "/customers/" + encodeURIComponent(id) + "/points_transactions", { query: query });
}
listTransactions(query) { return this.request("GET", "/points_transactions", { query: query }); }
getTransaction(id) { return this.request("GET", "/points_transactions/" + id); }
// Activities + redemptions
createActivity(body) { return this.request("POST", "/activities", { body: body }); }
// idempotencyKey is required by the API: persist it before calling and
// reuse it for every retry of the same redemption.
redeem(customerId, body, idempotencyKey) {
return this.request("POST", "/customers/" + encodeURIComponent(customerId) + "/redemptions", {
body: body,
idempotencyKey: idempotencyKey,
});
}
// Program (read-only)
listRewards(query) { return this.request("GET", "/rewards", { query: query }); }
getReward(id) { return this.request("GET", "/rewards/" + id); }
listEarnRules(query) { return this.request("GET", "/earn_rules", { query: query }); }
listVipTiers(query) { return this.request("GET", "/vip_tiers", { query: query }); }
getProgram() { return this.request("GET", "/program"); }
// Webhook endpoints
listWebhookEndpoints(query) { return this.request("GET", "/webhook_endpoints", { query: query }); }
createWebhookEndpoint(body) { return this.request("POST", "/webhook_endpoints", { body: body }); }
deleteWebhookEndpoint(id) { return this.request("DELETE", "/webhook_endpoints/" + id); }
}
// Verify a webhook signature header against the raw request body + your secret.
export function verifyWebhook(secret, signatureHeader, rawBody) {
const parts = {};
String(signatureHeader || "").split(",").forEach(function (p) {
const i = p.indexOf("=");
if (i > 0) parts[p.slice(0, i).trim()] = p.slice(i + 1).trim();
});
const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
try {
return crypto.timingSafeEqual(Buffer.from(parts.v1 || "", "hex"), Buffer.from(expected, "hex"));
} catch (e) {
return false;
}
}
export default Loyalino;