L Loyalino Developers

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.

bash
curl -o loyalino.js https://help.loyalino.io/sdk.js
No build step. It uses the built-in fetch and node:crypto, so there is nothing to install.

Quickstart

javascript
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

MethodCalls
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.

javascript
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.

javascript
// 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;