API reference
The Loyalino REST API lets you read and write loyalty data from your own backend. It is organized around predictable resources, uses standard HTTP verbs, and returns JSON for every response.
Introduction
The base URL for all endpoints is:
https://app.loyalino.io/api/v1Send and receive JSON. Every request must be made over HTTPS and authenticated with an API key. Resource ids, amounts, and timestamps are returned as stable, snake_case fields.
Authentication
Authenticate every request by passing your secret key as a bearer token in the Authorization header. Create and manage keys in your Loyalino admin under Settings, API keys. The secret is shown only once when you create it, so store it somewhere safe.
curl https://app.loyalino.io/api/v1/program \
-H "Authorization: Bearer lzk_live_your_secret_key"Keys come in two scopes. A read and write key can do everything. A read only key can call GET endpoints but is rejected with a 403 on any write. Never expose a secret key in client-side code or a public repository. If a key leaks, revoke it from the admin and create a new one.
Errors
Loyalino uses conventional HTTP status codes. Errors return a JSON body with a stable type, a machine readable code, and a human readable message.
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The API key is invalid or has been revoked."
}
}| Status | Meaning |
|---|---|
200 | OK. The request succeeded. |
201 | Created. A new resource was created. |
400 | Bad request, for example invalid JSON. |
401 | Missing, invalid, revoked, or expired API key. |
403 | The key does not have permission, for example a read only key on a write. |
404 | The resource does not exist. |
422 | Validation failed, for example a missing required field. |
429 | Too many requests. See rate limits. |
500 | Something went wrong on our side. |
Pagination
List endpoints return a list object and accept limit (default 20, maximum 100) and page (1 based) query parameters. The envelope includes the total count and whether more pages remain.
{
"object": "list",
"data": [ /* ... */ ],
"page": 1,
"limit": 20,
"total_count": 134,
"has_more": true
}Rate limits
Each API key may make up to 120 requests per minute. If you exceed the limit you receive a 429 response with a retry_after value in seconds. Back off and retry after that interval.
Customers
A customer is a loyalty member of your shop, with a points balance, lifetime points, and an optional VIP tier.
List your loyalty customers, most recent first. Filter with email for an exact match or search for a name or email substring.
| Field | Type | Description |
|---|---|---|
limit | integer | Page size, 1 to 100. Default 20. |
page | integer | Page number, 1 based. |
email | string | Return the customer with this exact email. |
search | string | Match name or email substring. |
curl https://app.loyalino.io/api/v1/customers?limit=1 \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [
{
"id": "cl9x2a1b0000",
"object": "customer",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"points_balance": 320,
"lifetime_points": 1200,
"pending_points": 0,
"vip_tier": "Gold",
"total_spent": 540,
"created_at": "2026-05-01T10:00:00.000Z"
}
],
"page": 1,
"limit": 1,
"total_count": 134,
"has_more": true
}Retrieve a single customer by Loyalino id, Shopify customer id, or email address (pass the email directly in the path).
curl https://app.loyalino.io/api/v1/customers/ada@example.com \
-H "Authorization: Bearer lzk_live_xxx"{
"id": "cl9x2a1b0000",
"object": "customer",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"points_balance": 320,
"lifetime_points": 1200,
"vip_tier": "Gold",
"total_spent": 540,
"created_at": "2026-05-01T10:00:00.000Z"
}Create a customer, or update the existing one with the same email. Requires a read and write key.
| Field | Type | Description |
|---|---|---|
email | string | Required. The customer email. |
first_name | string | Optional first name. |
last_name | string | Optional last name. |
birthday | string | Optional, format YYYY-MM-DD. |
shopify_customer_id | string | Optional Shopify customer id to link. |
curl https://app.loyalino.io/api/v1/customers \
-H "Authorization: Bearer lzk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"email":"grace@example.com","first_name":"Grace"}'{
"id": "cl9x9z0000",
"object": "customer",
"email": "grace@example.com",
"first_name": "Grace",
"points_balance": 0,
"lifetime_points": 0,
"vip_tier": null,
"created_at": "2026-06-07T12:00:00.000Z"
}Update a customer profile. Only the fields you send are changed. Requires a read and write key.
| Field | Type | Description |
|---|---|---|
first_name | string | New first name. |
last_name | string | New last name. |
birthday | string | New birthday, YYYY-MM-DD. |
curl -X PATCH https://app.loyalino.io/api/v1/customers/cl9x9z0000 \
-H "Authorization: Bearer lzk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"last_name":"Hopper"}'{
"id": "cl9x9z0000",
"object": "customer",
"email": "grace@example.com",
"first_name": "Grace",
"last_name": "Hopper",
"points_balance": 0
}Points transactions
A points transaction changes a customer balance. Award points with a positive value and deduct with a negative value. This is the primary way to manage balances over the API.
Create a points transaction for a customer. points must be a non-zero integer, and a deduction cannot drop the balance below zero. Requires a read and write key.
| Field | Type | Description |
|---|---|---|
points | integer | Required. Positive to award, negative to deduct. |
type | string | Optional label, for example earned or adjusted. |
description | string | Optional note shown in history. |
curl https://app.loyalino.io/api/v1/customers/cl9x2a1b0000/points_transactions \
-H "Authorization: Bearer lzk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"points":100,"type":"earned","description":"Birthday bonus"}'{
"id": "tx_71a2",
"object": "points_transaction",
"customer_id": "cl9x2a1b0000",
"points": 100,
"type": "earned",
"description": "Birthday bonus",
"created_at": "2026-06-07T12:05:00.000Z"
}List a single customer transactions, most recent first.
curl https://app.loyalino.io/api/v1/customers/cl9x2a1b0000/points_transactions \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [
{
"id": "tx_71a2",
"object": "points_transaction",
"customer_id": "cl9x2a1b0000",
"points": 100,
"type": "earned",
"created_at": "2026-06-07T12:05:00.000Z"
}
],
"page": 1,
"limit": 20,
"total_count": 1,
"has_more": false
}List transactions across all customers. Filter with customer_id or type.
| Field | Type | Description |
|---|---|---|
customer_id | string | Limit to one customer (id or email). |
type | string | Limit to a transaction type. |
limit | integer | Page size, 1 to 100. |
page | integer | Page number. |
curl "https://app.loyalino.io/api/v1/points_transactions?type=earned" \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [ /* points_transaction objects */ ],
"page": 1,
"limit": 20,
"total_count": 58,
"has_more": true
}Retrieve a single points transaction by id.
curl https://app.loyalino.io/api/v1/points_transactions/tx_71a2 \
-H "Authorization: Bearer lzk_live_xxx"{
"id": "tx_71a2",
"object": "points_transaction",
"customer_id": "cl9x2a1b0000",
"points": 100,
"type": "earned",
"created_at": "2026-06-07T12:05:00.000Z"
}Program
Read-only endpoints that describe how your program is configured: the rewards customers can redeem, the ways they earn, your VIP tiers, and your points settings.
List redemption rewards. Add ?enabled=true to return only active rewards.
curl https://app.loyalino.io/api/v1/rewards?enabled=true \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [
{
"id": "rw_5",
"object": "reward",
"name": "5 off",
"type": "amount_discount",
"points_cost": 100,
"discount_value": 5,
"enabled": true,
"variable": null
}
],
"page": 1,
"limit": 20,
"total_count": 6,
"has_more": false
}List the ways customers earn points. Add ?enabled=true for active rules only.
curl https://app.loyalino.io/api/v1/earn_rules \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [
{
"id": "er_1",
"object": "earn_rule",
"name": "Place an order",
"type": "place_order",
"points": 1,
"enabled": true
}
],
"page": 1,
"limit": 20,
"total_count": 5,
"has_more": false
}List your VIP tiers, lowest threshold first.
curl https://app.loyalino.io/api/v1/vip_tiers \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [
{
"id": "vt_2",
"object": "vip_tier",
"name": "Gold",
"points_required": 1000,
"multiplier": 1.5,
"discount_type": "percentage",
"discount_value": 10,
"free_shipping": true
}
],
"page": 1,
"limit": 20,
"total_count": 3,
"has_more": false
}Retrieve your program settings and status: whether it is live, your plan, the earn rate, and the currency.
curl https://app.loyalino.io/api/v1/program \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "program",
"shop_domain": "your-store.myshopify.com",
"live": true,
"plan": "growth",
"points_per_unit": 1,
"currency": "USD",
"points_delay_days": 0,
"points_expiry_days": 365
}Activities and redemptions
Award points for any custom action, or redeem a reward end to end.
Record a customer activity and award points. Provide explicit points, or omit them to use the matching enabled earn rule for type. Requires a read and write key.
| Field | Type | Description |
|---|---|---|
customer_id | string | Required. Internal id, Shopify id, or email. |
type | string | Required. The activity name (also matched to an earn rule). |
points | integer | Optional. Overrides the matching rule's points. |
curl https://app.loyalino.io/api/v1/activities \
-H "Authorization: Bearer lzk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"customer_id":"ada@example.com","type":"newsletter_signup","points":50}'{
"object": "activity",
"type": "newsletter_signup",
"customer_id": "cl9x2a1b0000",
"points_awarded": 50,
"transaction": {
"id": "tx_a1",
"object": "points_transaction",
"points": 50,
"type": "earned"
}
}Redeem a reward for a customer. Uses the same pipeline as the storefront: atomic points deduction, Shopify discount minting (or native store credit), and rollback on failure. Pass points_to_redeem only for variable rewards. Requires a read and write key.
| Field | Type | Description |
|---|---|---|
reward_id | string | Required. The reward to redeem. |
points_to_redeem | integer | Required only for variable-redemption rewards. |
curl https://app.loyalino.io/api/v1/customers/cl9x2a1b0000/redemptions \
-H "Authorization: Bearer lzk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"reward_id":"rw_5"}'{
"id": "LOYALTY7F3A2B",
"object": "redemption",
"customer_id": "cl9x2a1b0000",
"reward_id": "rw_5",
"points_spent": 100,
"remaining_balance": 220,
"discount_code": "LOYALTY7F3A2B",
"apply_mode": "code"
}Webhooks
Register HTTPS endpoints to receive events the moment they happen. Manage them here or in your admin under Settings, Webhooks. This is a capability most loyalty apps, including Smile, do not offer merchants.
| Event | When it fires |
|---|---|
points.created | Points are awarded (order, API, or activity). |
customer.created | A new loyalty customer is created. |
customer.updated | A customer profile changes. |
redemption.created | A reward is redeemed. |
tier.changed | A customer moves VIP tier. |
Each delivery is a signed POST with this JSON body:
{
"id": "evt_3a9f1c",
"object": "event",
"type": "points.created",
"created_at": "2026-06-07T12:00:00.000Z",
"data": { "customer_id": "cl9x2a1b0000", "points": 100 }
}Verify signatures
Every request carries a Loyalino-Signature: t=<unix>,v1=<hmac> header. Recompute the HMAC-SHA256 of the timestamp, a dot, and the raw request body using your endpoint secret, then compare in constant time. The Node SDK exports verifyWebhook for this.
import crypto from "node:crypto";
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(parts.v1, "hex"),
Buffer.from(expected, "hex"),
);
}List your webhook endpoints. The signing secret is only ever returned once, at creation.
curl https://app.loyalino.io/api/v1/webhook_endpoints \
-H "Authorization: Bearer lzk_live_xxx"{
"object": "list",
"data": [
{
"id": "we_1",
"object": "webhook_endpoint",
"url": "https://you.example.com/hook",
"events": ["*"],
"enabled": true,
"last_status": 200
}
],
"page": 1,
"limit": 20,
"total_count": 1,
"has_more": false
}Register an endpoint. The response includes the signing secret once. Subscribe to specific events, or omit events for all. Requires a read and write key.
| Field | Type | Description |
|---|---|---|
url | string | Required. A public https URL. |
events | array | Optional. Event names to subscribe to, or omit for all. |
description | string | Optional label. |
curl https://app.loyalino.io/api/v1/webhook_endpoints \
-H "Authorization: Bearer lzk_live_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://you.example.com/hook","events":["points.created","redemption.created"]}'{
"id": "we_1",
"object": "webhook_endpoint",
"url": "https://you.example.com/hook",
"events": ["points.created", "redemption.created"],
"enabled": true,
"secret": "whsec_9f2a7c..."
}Delete a webhook endpoint. You can also PATCH it to change the url, events, or enabled state.
curl -X DELETE https://app.loyalino.io/api/v1/webhook_endpoints/we_1 \
-H "Authorization: Bearer lzk_live_xxx"{
"id": "we_1",
"object": "webhook_endpoint",
"deleted": true
}