LesyLoyalty API v1
Connect an ERP, a back-office system or your own database to your loyalty program: read members and balances, adjust points, send sales recorded outside Shopify (wholesale, phone or event orders), redeem rewards and check gift card balances. Webhooks notify your system whenever something changes. In-store sales go through Shopify POS, where LesyLoyalty is built in.
Base URL: https://app.lesyloyalty.com/api/v1 · JSON in, JSON out · Available on the Pro plan.
Authentication
Create a key in the LesyLoyalty app: Configuration › API keys. Each key has its own permissions and can be revoked at any time. Send it in the Authorization header:
curl https://app.lesyloyalty.com/api/v1/rewards \
-H "Authorization: Bearer llk_your_key"
| Permission | Allows |
|---|---|
read | Find members, transactions, rewards |
points | Add or remove points |
orders | Send, read, refund and cancel external sales |
redeem | Redeem rewards for a member |
giftcards | No longer used: gift cards are read-only through the API (reading them needs read) |
Keep keys on your server. Never put them in a web page or a mobile app: anyone holding a key can act on your program.
Errors
Errors use standard HTTP status codes and always have the same shape:
{ "error": { "code": "insufficient_points", "message": "Not enough points", "balance": 120 } }
| Status | Codes |
|---|---|
| 400 | invalid_json, invalid_customer_id, invalid_points, invalid_line, lines_required, customer_required… |
| 401 | unauthorized — missing, invalid or revoked key |
| 403 | plan_required, insufficient_scope, member_excluded |
| 404 | customer_not_found, not_member, order_not_found, reward_not_found |
| 409 | insufficient_points, ambiguous_customer, insufficient_balance, gift_card_unusable, ambiguous_gift_card, code_taken, request_in_progress |
| 422 | customer_create_failed |
| 429 | rate_limited — see Retry-After |
| 5xx | server_error, shopify_unavailable — safe to retry |
Rate limits
120 requests per minute per key. Above that, the API answers 429 with a Retry-After header (seconds).
Duplicates
Retries are safe. A sale is identified by your external_id, a refund by your refund_id: sending them again never credits or removes points twice (the response says "status": "duplicate"). For point adjustments, send an idempotency_key. Gift card operations that change money require one.
Find a member
Members are Shopify customers. Search by numeric Shopify customer ID, email or phone (E.164, e.g. +15145550123). Optional locale (e.g. fr, en) for tier and reward names. Permission: read.
curl "https://app.lesyloyalty.com/api/v1/members?email=jane@example.com" \
-H "Authorization: Bearer llk_your_key"
{
"member": {
"customer_id": "7712345678901",
"email": "jane@example.com",
"first_name": "Jane",
"balance": 1250,
"lifetime_earned": 3400,
"excluded": false,
"tier": { "id": "cm…", "name": "Gold", "multiplier": 1.5, "spent_this_period": 812.4 },
"birthday": "1990-03-07",
"referral_code": "FRIEND-JANE1",
"enrolled_at": "2026-05-02T14:21:09.000Z",
"codes": [
{ "code": "REWARD-KC5MGH43", "reward": "$10 off", "points_spent": 1000,
"expires_at": "2027-05-02T14:21:09.000Z", "created_at": "2026-05-02T14:21:09.000Z" }
]
}
}
404 not_member means the customer exists in Shopify but has not joined the program yet; any point adjustment, sale or redemption enrolls them automatically.
Transactions
Most recent first, up to 250 per page. To get the next page, pass next_before as before. Permission: read.
{
"transactions": [
{ "id": "cm…", "created_at": "2026-10-20T18:44:38.777Z", "type": "purchase", "delta": 240,
"balance_after": 1250, "description": "Order #1042", "reference": "5551234567890" }
],
"has_more": true,
"next_before": "2026-09-30T12:00:00.000Z"
}
Types: purchase, refund, cancel, signup, birthday, referral, social, review, redeem, redeem_cancel, manual, expire, adjustment.
Adjust points
Positive to add, negative to remove. Permission: points.
curl -X POST https://app.lesyloyalty.com/api/v1/members/7712345678901/points \
-H "Authorization: Bearer llk_your_key" -H "Content-Type: application/json" \
-d '{ "points": 500, "reason": "Workshop attendance", "idempotency_key": "workshop-2026-10-21-jane" }'
| Field | |
|---|---|
points | Integer, not zero, between -1,000,000 and 1,000,000 |
reason | 1 to 120 characters, shown in the member's history |
idempotency_key | Optional, 1 to 64 characters: the same key is only applied once |
{ "applied": 500, "member": { "customer_id": "7712345678901", "balance": 1750, … } }
Removing more points than the balance returns 409 insufficient_points unless negative balances are enabled in the program settings.
List rewards
Active rewards, in display order. Permission: read.
{ "rewards": [
{ "id": "cm…", "type": "fixed_amount", "label": "$10 off", "points_cost": 1000,
"value": 10, "min_order_amount": null, "variant_id": null }
] }
Redeem a reward
Spends the points and creates the discount code (or gift card) in Shopify, exactly like a redemption from the store widget. Permission: redeem.
curl -X POST https://app.lesyloyalty.com/api/v1/members/7712345678901/redeem \
-H "Authorization: Bearer llk_your_key" -H "Content-Type: application/json" \
-d '{ "reward_id": "cm…", "locale": "en" }'
{ "code": "REWARD-8Q2WXT7M", "label": "$10 off", "expires_at": "2027-10-21T13:00:00.000Z",
"variant_id": null, "member": { "balance": 750, … } }
Send a sale
For sales recorded outside Shopify in an ERP or back-office system (wholesale, phone or event orders). Points are calculated with the same rules as Shopify orders: earning rate, tier multiplier, active points promotions, excluded tags, gift cards. Reward codes listed in discount_codes are marked as used. Permission: orders.
curl -X POST https://app.lesyloyalty.com/api/v1/orders \
-H "Authorization: Bearer llk_your_key" -H "Content-Type: application/json" \
-d '{
"external_id": "ERP-000123",
"name": "Store #123",
"customer": { "email": "jane@example.com", "first_name": "Jane", "create": true },
"lines": [
{ "title": "Candle 8 oz", "quantity": 2, "price": 24.00, "discount": 4.00, "product_id": 8123456789012 },
{ "title": "Room mist", "quantity": 1, "price": 18.00 }
],
"discount_codes": ["REWARD-8Q2WXT7M"]
}'
| Field | |
|---|---|
external_id | Required. Your unique sale ID: 1 to 64 characters (letters, digits, . _ : -) |
customer | Required. customer_id, email or phone. With "create": true (and optional first_name, last_name) the Shopify customer is created if not found. |
lines | title, quantity, price (unit price), discount (total for the line), optional product_id / variant_id (Shopify IDs, needed for product or collection promotions), sku, gift_card. Up to 500 lines. |
amount | Instead of lines: products total after discounts, before taxes and shipping |
name, discount_codes, tags, locale | Optional |
Amounts are in the store currency, products only (no taxes, no shipping).
{ "status": "credited", "external_id": "ERP-000123", "points": 112,
"customer_id": "7712345678901", "member": { "balance": 1362, … } }
status: credited (201), duplicate (already sent) or skipped with a reason (order_tag, customer_tag, member_excluded, b2b_company).
Sale status
{ "order": { "external_id": "ERP-000123", "name": "Store #123", "customer_id": "7712345678901",
"base_amount": 62, "refunded_amount": 0, "points_credited": 112, "points_removed": 0,
"cancelled": false, "tier": "Gold", "campaigns": [], "created_at": "2026-10-21T13:00:00.000Z" } }
Refund
Removes points in proportion to the refunded amount (products, before taxes). Permission: orders.
curl -X POST https://app.lesyloyalty.com/api/v1/orders/ERP-000123/refunds \
-H "Authorization: Bearer llk_your_key" -H "Content-Type: application/json" \
-d '{ "refund_id": "RF-77", "amount": 18.00 }'
{ "status": "refunded", "points": -33 }
Cancel
Removes all the points still credited for the sale. Permission: orders.
{ "status": "cancelled", "points": -79 }
Gift cards
Gift cards are Shopify gift cards, the same cards customers use on your online store and in Shopify POS, with one shared balance. Through the API they are read-only (permission read): find a card and check its balance. Cards are sold, topped up, charged and deactivated in Shopify (Shopify POS or the Shopify admin), and refunds always go back to the original payment method in Shopify. Write calls (POST /giftcards, /debit, /credit, /deactivate) return 410 gift_cards_read_only. Amounts are in the card currency (your store currency).
{
"gift_card": {
"id": "584736251",
"last_characters": "7K2Q",
"balance": 42.5,
"initial_value": 50,
"currency": "CAD",
"enabled": true,
"expired": false,
"expires_on": null,
"customer_id": "7712345678901",
"note": "Loyalty reward",
"created_at": "2026-10-21T13:00:00Z"
}
}
Find a card
curl "https://app.lesyloyalty.com/api/v1/giftcards?code=X7HG2MPQ9R4W7K2Q" \
-H "Authorization: Bearer llk_your_key"
{ "gift_cards": [ { "id": "584736251", "last_characters": "7K2Q", "balance": 42.5, "enabled": true, … } ] }
By customer, deactivated cards are hidden unless you add include_disabled=true. An empty list means no card matches.
Webhooks
Add up to 5 HTTPS endpoints in Configuration › API keys › Outgoing webhooks and choose their events. LesyLoyalty sends a POST with a JSON body:
{
"id": "6f1c2c1e-7a0e-4d1f-9a3e-0e3b5d2a9c11",
"event": "points.changed",
"created_at": "2026-10-21T13:00:02.114Z",
"data": { … }
}
| Header | |
|---|---|
X-LesyLoyalty-Event | Event name |
X-LesyLoyalty-Delivery | Delivery ID (the same on retries) |
X-LesyLoyalty-Timestamp | Unix time (seconds) of this attempt |
X-LesyLoyalty-Signature | sha256= + HMAC-SHA256 of timestamp + "." + raw body with the endpoint secret |
Answer with any 2xx within 10 seconds. Otherwise the delivery is retried 5 times: after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Events can arrive more than once or out of order: use the body id to ignore duplicates.
Verify signatures
Always verify the signature with the raw body (before JSON parsing) and reject timestamps older than 5 minutes.
Node.js
const crypto = require("crypto");
function verify(rawBody, headers, secret) {
const ts = headers["x-lesyloyalty-timestamp"];
const sig = headers["x-lesyloyalty-signature"] || "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Python
import hmac, hashlib, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers["X-LesyLoyalty-Timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(headers.get("X-LesyLoyalty-Signature", ""), expected)
PHP
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_LESYLOYALTY_TIMESTAMP'];
$sig = $_SERVER['HTTP_X_LESYLOYALTY_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
$valid = abs(time() - (int) $ts) <= 300 && hash_equals($expected, $sig);
Events
| Event | data |
|---|---|
points.changed | customer_id, transaction_id, delta, balance, lifetime_earned, type, description, reference |
tier.changed | customer_id, previous_tier (id, name or null), tier (id, name, multiplier), direction (up | down) |
reward.redeemed | customer_id, reward_id, label, code, expires_at, balance |
member.created | customer_id, email, first_name, balance, enrolled_at |
giftcard.created | Card created through LesyLoyalty (Shopify POS tile, reward redeemed for a gift card). gift_card_id, last_characters, balance, initial_value, currency, enabled, expires_on, customer_id, order_id (Shopify order that sold it, or null), updated_at |
giftcard.updated | Same fields, current state after the change: balance used, topped up or refunded, card deactivated (enabled: false, disabled_at) or expiry changed |
ping | Sent by the « Send test » button: message |
Every change of points sends points.changed, whatever its origin: Shopify orders, the store widget, Shopify POS, the admin or this API.