Loyalo API & webhooks
Connect your own systems to Loyalo: subscriptions, customers and points over REST, plus signed webhooks with curl and JavaScript examples.
Authentication
Create a key in Loyalo under Settings → API & webhooks. Send it in the x-api-key header (or as a Bearer token). A key always belongs to a single shop and only works for the scopes you enable. Keys cannot be retrieved after creation: store them safely on your side.
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/ping" \
-H "x-api-key: lo_live_xxxxxxxxxxxxxxxx" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/ping", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const body = await res.json();
console.log(body); // { ok: true, shop: "...", scopes: [...] } Endpoints
All responses are JSON shaped as { ok: true, data: … }. Lists also return next_offset (null on the last page). Errors return { error: "…" } with a matching status code.
/subscriptionsscope: subscriptionsList your shop's subscriptions, newest first.
Query parameters: status (active | paused | cancelled | expired | failed), customer_id, email, limit (max 200), offset
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions" \
-H "x-api-key: $LOYALO_API_KEY" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const { data, next_offset } = await res.json(); /subscriptions/{id}scope: subscriptionsA single subscription with lines, amount and next billing date.
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID" \
-H "x-api-key: $LOYALO_API_KEY" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const { data } = await res.json(); /subscriptions/{id}/pausescope: subscriptions:writePauses the subscription (both Shopify and Mollie).
curl -X POST "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/pause" \
-H "x-api-key: $LOYALO_API_KEY" \
-H "Content-Type: application/json" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/pause", {
method: "POST",
headers: {
"x-api-key": process.env.LOYALO_API_KEY,
"Content-Type": "application/json",
},
});
const { data } = await res.json(); /subscriptions/{id}/resumescope: subscriptions:writeResumes a paused subscription.
curl -X POST "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/resume" \
-H "x-api-key: $LOYALO_API_KEY" \
-H "Content-Type: application/json" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/resume", {
method: "POST",
headers: {
"x-api-key": process.env.LOYALO_API_KEY,
"Content-Type": "application/json",
},
});
const { data } = await res.json(); /subscriptions/{id}/skipscope: subscriptions:writeSkips the next delivery; the date moves one interval forward.
curl -X POST "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/skip" \
-H "x-api-key: $LOYALO_API_KEY" \
-H "Content-Type: application/json" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/skip", {
method: "POST",
headers: {
"x-api-key": process.env.LOYALO_API_KEY,
"Content-Type": "application/json",
},
});
const { data } = await res.json(); /subscriptions/{id}/reschedulescope: subscriptions:writeMoves the next delivery to another date (within a year).
curl -X POST "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/reschedule" \
-H "x-api-key: $LOYALO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "date": "2026-11-15" }' const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/reschedule", {
method: "POST",
headers: {
"x-api-key": process.env.LOYALO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ "date": "2026-11-15" }),
});
const { data } = await res.json(); /subscriptions/{id}/cancelscope: subscriptions:writeCancels the subscription permanently.
curl -X POST "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/cancel" \
-H "x-api-key: $LOYALO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "customer_request" }' const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/subscriptions/SUBSCRIPTION_ID/cancel", {
method: "POST",
headers: {
"x-api-key": process.env.LOYALO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ "reason": "customer_request" }),
});
const { data } = await res.json(); /customersscope: customersCustomers with point balance, spend and language preference.
Query parameters: email (exact match), limit (max 200), offset
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/customers" \
-H "x-api-key: $LOYALO_API_KEY" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/customers", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const { data, next_offset } = await res.json(); /pointsscope: pointsPoint transactions, optionally filtered by customer.
Query parameters: customer_id, limit (max 500), offset
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/points" \
-H "x-api-key: $LOYALO_API_KEY" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/points", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const { data, next_offset } = await res.json(); /pointsscope: points:writeAdd (positive) or remove (negative) points. Give the customer by customer_id (Loyalo's), shopify_customer_id or email: exactly one. The same idempotency_key (or Idempotency-Key header) never books twice.
curl -X POST "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/points" \
-H "x-api-key: $LOYALO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "customer_id": "…", "points": 100, "note": "Goodwill", "idempotency_key": "ticket-4711" }' const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/points", {
method: "POST",
headers: {
"x-api-key": process.env.LOYALO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ "customer_id": "…", "points": 100, "note": "Goodwill", "idempotency_key": "ticket-4711" }),
});
const { data } = await res.json(); /redemptionsscope: redemptionsRedeemed discounts with code, status and expiry.
Query parameters: customer_id, status (pending | issued | failed | used | expired), limit (max 200), offset
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/redemptions" \
-H "x-api-key: $LOYALO_API_KEY" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/redemptions", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const { data, next_offset } = await res.json(); /pingscope: —Validates your key and returns the shop and its scopes.
curl "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/ping" \
-H "x-api-key: $LOYALO_API_KEY" const res = await fetch("https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/ping", {
headers: { "x-api-key": process.env.LOYALO_API_KEY },
});
const { data } = await res.json(); Error codes
| Status | Message | Meaning |
|---|---|---|
| 401 | Missing API key / Invalid API key | Key missing, wrong or revoked. |
| 403 | Sleutel mist recht '…' | The key lacks this scope. Adjust it in Loyalo. |
| 404 | Not found | Unknown path, or the object does not exist in this shop. |
| 429 | Rate limit exceeded | More requests than the per-minute limit. Back off and retry. |
| 500 | Foutmelding in het veld error | Something failed on our side or at Shopify/Mollie. |
AI assistants (MCP)
Loyalo is also an MCP server: an AI assistant such as Claude or ChatGPT uses the same functions as tools. Connect with the address below and an API key as Bearer token. The assistant only sees the tools that key's scopes allow; give it its own key, with write scopes only if it may change subscriptions or points.
claude mcp add --transport http loyalo https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/mcp \
--header "Authorization: Bearer lo_live_xxxxxxxxxxxxxxxx" {
"mcpServers": {
"loyalo": {
"type": "http",
"url": "https://mwugkfkidoakjoelhtir.supabase.co/functions/v1/v2-api/mcp",
"headers": { "Authorization": "Bearer lo_live_xxxxxxxxxxxxxxxx" }
}
}
} Webhooks
Configure an endpoint in Loyalo with the events you want. Every delivery is signed and retried on failure with increasing back-off (1, 5, 20, 60 and 240 minutes). Respond with a 2xx status within 10 seconds.
| Event | When |
|---|---|
subscription.created | New subscription created |
subscription.updated | Subscription changed (date, lines, frequency) |
subscription.paused | Subscription paused |
subscription.resumed | Subscription resumed |
subscription.cancelled | Subscription cancelled |
subscription.payment_failed | Payment attempt failed |
points.changed | A customer's point balance changed |
ping | Test message (button in Loyalo) |
Example delivery
POST https://jouw-server.nl/loyalo-webhook
x-loyalo-event: subscription.paused
x-loyalo-timestamp: 1755763200
x-loyalo-signature: 4f3a… (HMAC-SHA256 van "timestamp.body")
{
"id": "7b1c…",
"event": "subscription.paused",
"created_at": "2026-08-21T08:00:00.000Z",
"data": {
"id": "9f2e…",
"external_id": "gid://shopify/SubscriptionContract/123",
"status": "paused",
"source": "shopify",
"customer_email": "klant@voorbeeld.nl",
"amount": 24.95,
"currency": "EUR",
"next_billing_date": "2026-09-01T00:00:00.000Z",
"billing_interval": "MONTH",
"billing_interval_count": 1,
"lines": [{ "title": "Koffie 1kg", "quantity": 1 }]
}
} Verifying the signature
The signature is an HMAC-SHA256 over "timestamp.body" using your endpoint secret (starts with whsec_). Compare in constant time and reject stale requests.
import crypto from "node:crypto";
// Express-voorbeeld met de ruwe body (Buffer)
function verifyLoyaloWebhook(req, secret) {
const timestamp = req.header("x-loyalo-timestamp");
const signature = req.header("x-loyalo-signature");
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${req.rawBody}`)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature ?? ""),
);
// Weiger verzoeken ouder dan 5 minuten
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return ok && fresh;
}