# Authentication
Source: https://developers.mageloyalty.com/api-reference/authentication
Authenticate requests to the Mage Loyalty API using API keys.
## API keys
All API requests must include your API key in the `Authorization` header. No `Bearer` prefix is needed.
```bash theme={null}
curl https://api.mageloyalty.com/v1/customers \
-H "Authorization: your-api-key-here"
```
API keys grant access to your loyalty program data, including customer details. Both key types are server-side only. Never embed an API key in a storefront widget, mobile app, or any client-side code. To build storefront and browser-facing experiences, use the [JavaScript SDK](/js-sdk/introduction), which scopes data to the logged-in customer.
## Create an API key
In the Mage Loyalty dashboard, go to **Settings > API Keys**. API keys are available on the Growth plan and above.
Give the key a name so you can recognize where it is used, choose **Full access** or **Read-only** (see [Scopes](#scopes)), and generate it.
The key is shown only once, at creation. Copy it and store it somewhere secure. If you lose it, revoke the key and generate a new one.
You can revoke a key at any time from the same screen, which immediately stops it from authenticating. Revoked keys can then be deleted.
## Scopes
Each API key has one of two access levels:
| Scope | Description |
| - | - |
| **Full access** | Can perform all read and write operations |
| **Read only** | Can only perform `GET` requests |
Use **read-only keys** if you do not wish to manipulate any loyalty data via the API. Read-only keys still read all of your loyalty data, including customer details, so they are not a public credential. Keep both key types server-side.
Attempting a write operation with a read-only key returns `403 Forbidden`:
```json theme={null}
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "This API key is read-only and cannot perform write operations"
}
]
}
```
## Missing or invalid key
Requests without an `Authorization` header, or with a key that is unknown or has been revoked, return `401 Unauthorized`. A request made with an invalid key looks like this:
```bash cURL theme={null}
curl https://api.mageloyalty.com/v1/customers \
-H "Authorization: revoked-or-unknown-key"
```
```javascript Node theme={null}
const response = await fetch("https://api.mageloyalty.com/v1/customers", {
headers: {
Authorization: "revoked-or-unknown-key",
},
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers",
headers={"Authorization": "revoked-or-unknown-key"},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: revoked-or-unknown-key",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 401 Missing header theme={null}
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "Missing Authorization header"
}
]
}
```
```json 401 Invalid key theme={null}
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "Invalid or revoked API key"
}
]
}
```
```json 403 Read-only key theme={null}
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "This API key is read-only and cannot perform write operations"
}
]
}
```
# Adjust points
Source: https://developers.mageloyalty.com/api-reference/customers/adjust-points
POST /v1/customers/{identifier}/points
Manually add or deduct points for a customer.
Manually add or deduct points for a customer. Useful for CS teams handling complaints, migrations from another platform, or any bespoke earning scenario not covered by earning rules.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `adjustment` | integer | Yes | Points to add (positive) or deduct (negative). Cannot be zero. On store-credit shops this is a minor-unit amount, so `500` means 5.00 of credit (see [Loyalty modes](/api-reference/loyalty-modes)). |
| `note` | string | No | Internal note stored on the audit record |
| `update_lifetime_points` | boolean | No | Whether to also adjust lifetime points. Default: `true` |
| `send_email` | boolean | No | Whether to notify the customer of the adjustment. This gates both the points-earned email and the events sent to connected marketing integrations. Default: `true` |
```json theme={null}
{
"adjustment": 500,
"note": "Goodwill gesture after delayed order",
"update_lifetime_points": true,
"send_email": true
}
```
## Behaviour
* Points balance floors at `0` — deductions will never produce a negative balance
* Customer tier is recalculated automatically after the adjustment
* Creates an audit trail entry in the customer's earning history
## Response
Returns `200 OK` with a `points_adjustment` object.
| Field | Type | Description |
| - | - | - |
| `shopify_customer_id` | string | The customer's Shopify ID |
| `adjustment` | integer | The applied adjustment (minor units in store-credit mode) |
| `points_before` | integer | Balance before the adjustment |
| `points_after` | integer | Balance after the adjustment |
| `current_points` | integer | Balance after the adjustment (same as `points_after`) |
| `loyalty_mode` | string | Either `points` or `store_credit` |
| `currency` | string | The shop's ISO 4217 currency code |
On store-credit shops the response also includes `credit_*` companions with the major-unit decimal strings: `credit_adjustment`, `credit_before`, `credit_after`, and `credit_balance`. These are absent in points mode. See [Loyalty modes](/api-reference/loyalty-modes).
## Errors
| Status | Scenario |
| - | - |
| `400` | `adjustment` missing, zero, or not an integer |
| `403` | Customer is excluded from the loyalty program |
| `404` | Customer not found |
| `500` | The points adjustment could not be applied |
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/customers/6789012345/points \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"adjustment": 500,
"note": "Goodwill gesture after delayed order",
"update_lifetime_points": true,
"send_email": true
}'
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345/points",
{
method: "POST",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
adjustment: 500,
note: "Goodwill gesture after delayed order",
update_lifetime_points: true,
send_email: true,
}),
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/customers/6789012345/points",
headers={"Authorization": ""},
json={
"adjustment": 500,
"note": "Goodwill gesture after delayed order",
"update_lifetime_points": True,
"send_email": True,
},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"adjustment" => 500,
"note" => "Goodwill gesture after delayed order",
"update_lifetime_points" => true,
"send_email" => true,
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "points_adjustment",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"adjustment": 500,
"points_before": 200,
"points_after": 700,
"current_points": 700,
"loyalty_mode": "points",
"currency": "GBP"
}
}
}
```
```json 200 OK (store-credit shop) theme={null}
{
"data": {
"type": "points_adjustment",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"adjustment": 500,
"points_before": 200,
"points_after": 700,
"current_points": 700,
"loyalty_mode": "store_credit",
"currency": "GBP",
"credit_adjustment": "5.00",
"credit_before": "2.00",
"credit_after": "7.00",
"credit_balance": "7.00"
}
}
}
```
```json 403 Forbidden theme={null}
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "Customer is excluded from the loyalty program"
}
]
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# Enroll a customer
Source: https://developers.mageloyalty.com/api-reference/customers/enroll
POST /v1/customers
Enroll a customer in the loyalty program.
Enroll a customer in the loyalty program. If the customer is already enrolled, their existing record is returned with `200 OK` instead.
Only `shopify_customer_id` is required — the endpoint fetches the customer's name, email, and account creation date directly from Shopify.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `shopify_customer_id` | string | Yes | The customer's Shopify customer ID |
```json theme={null}
{
"shopify_customer_id": "6789012345"
}
```
## Response
Returns `201 Created` on successful enrollment, or `200 OK` if the customer was already enrolled.
The response shape matches the [customer object](/api-reference/customers/object).
## Errors
| Status | Scenario |
| - | - |
| `400` | `shopify_customer_id` missing or invalid JSON |
| `404` | No Shopify customer found with that ID |
| `503` | Unable to connect to Shopify |
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/customers \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"shopify_customer_id": "6789012345"
}'
```
```javascript Node theme={null}
const response = await fetch("https://api.mageloyalty.com/v1/customers", {
method: "POST",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
shopify_customer_id: "6789012345",
}),
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/customers",
headers={"Authorization": ""},
json={"shopify_customer_id": "6789012345"},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"shopify_customer_id" => "6789012345",
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 201 Created theme={null}
{
"data": {
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 0,
"lifetime_points": 0,
"redeemed_points": 0,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": null,
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Bronze",
"threshold": 0,
"points_multiplier": 1,
"badge_image_url": null,
"badge_color": "#CD7F32"
}, "created_at": "2026-06-23T12:00:00.000Z",
"updated_at": "2026-06-23T12:00:00.000Z"
}
}
}
```
```json 200 Already Enrolled theme={null}
{
"data": {
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Gold",
"threshold": 500,
"points_multiplier": 1.5,
"badge_image_url": null,
"badge_color": "#FFD700"
}, "created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z"
}
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Shopify customer '6789012345' not found"
}
]
}
```
```json 503 Service Unavailable theme={null}
{
"errors": [
{
"status": "503",
"title": "Service Unavailable",
"detail": "Unable to connect to Shopify. Please try again."
}
]
}
```
# Retrieve a customer
Source: https://developers.mageloyalty.com/api-reference/customers/get
GET /v1/customers/{identifier}
Retrieve a single customer by Shopify ID or email.
Fetch a single customer. The `identifier` can be:
* A **Shopify customer ID** (numeric string, e.g. `6789012345`)
* An **email address** (e.g. `jane@example.com`)
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `exclude_vip` | `true` | When set to `true`, omits the VIP tier lookup and returns `vip_tier` as `null` |
## Response
Returns the [customer object](/api-reference/customers/object). When `exclude_vip=true`, the `vip_tier` field is `null`.
## Errors
| Status | Scenario |
| - | - |
| `404` | Customer not found |
```bash cURL theme={null}
# By Shopify customer ID
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345 \
--header 'Authorization: '
# By email address
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/jane@example.com \
--header 'Authorization: '
```
```javascript Node theme={null}
// By Shopify customer ID
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
# By email address
response = requests.get(
"https://api.mageloyalty.com/v1/customers/jane@example.com",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Gold",
"threshold": 500,
"points_multiplier": 1.5,
"badge_image_url": null,
"badge_color": "#FFD700"
}, "created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z"
}
}
}
```
```json 200 OK (exclude_vip=true) theme={null}
{
"data": {
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": null, "created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z"
}
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# Get anniversary
Source: https://developers.mageloyalty.com/api-reference/customers/get-anniversary
GET /v1/customers/{identifier}/anniversary
Get a customer's loyalty anniversary date.
Get the date the customer created their Shopify account, used as their loyalty anniversary. The `identifier` can be a Shopify customer ID or email.
## Response
`anniversary_date` will be `null` if the Shopify account creation date has not been synced.
## Errors
| Status | Scenario |
| - | - |
| `404` | Customer not found |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL (Shopify ID) theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345/anniversary \
--header 'Authorization: '
```
```bash cURL (email) theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/jane@example.com/anniversary \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345/anniversary",
{
method: "GET",
headers: { Authorization: "" },
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers/6789012345/anniversary",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => ["Authorization: "],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "customer_anniversary",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"anniversary_date": "2023-03-10T09:00:00.000Z"
}
}
}
```
```json 200 OK (account date not synced) theme={null}
{
"data": {
"type": "customer_anniversary",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"anniversary_date": null
}
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# Get birthday
Source: https://developers.mageloyalty.com/api-reference/customers/get-birthday
GET /v1/customers/{identifier}/birthday
Get a customer's date of birth.
Get the customer's date of birth. The `identifier` can be a Shopify customer ID or email.
## Response
`date_of_birth` will be `null` if the customer has not provided their date of birth.
## Errors
| Status | Scenario |
| - | - |
| `404` | Customer not found |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL (Shopify ID) theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345/birthday \
--header 'Authorization: '
```
```bash cURL (email) theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/jane@example.com/birthday \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345/birthday",
{
method: "GET",
headers: { Authorization: "" },
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers/6789012345/birthday",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => ["Authorization: "],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "customer_birthday",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"date_of_birth": "1990-05-15T00:00:00.000Z"
}
}
}
```
```json 200 OK (no birthday on file) theme={null}
{
"data": {
"type": "customer_birthday",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"date_of_birth": null
}
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# Get earning history entry
Source: https://developers.mageloyalty.com/api-reference/customers/get-earning-history
GET /v1/customers/{identifier}/earning-history/{id}
Get a single earning history entry.
Get a single earning history entry by its ID. Validates that the entry belongs to the given customer and shop.
## Response
Returns a single [earning history entry](/api-reference/customers/list-earning-history) object. The response `meta` includes `loyalty_mode` and `currency` so you can read the `points` value correctly (see [Loyalty modes](/api-reference/loyalty-modes)).
## Errors
| Status | Scenario |
| - | - |
| `404` | Entry or customer not found |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345/earning-history/entry_abc \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345/earning-history/entry_abc",
{
method: "GET",
headers: { Authorization: "" },
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers/6789012345/earning-history/entry_abc",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => ["Authorization: "],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "earning_history_entry",
"id": "entry_abc",
"attributes": {
"earning_rule_id": "rule_abc",
"action": "purchase",
"points": 150,
"base_points": 100,
"points_multiplier": 1.5,
"status": "approved",
"order_id": "5678901234",
"order_number": "#1042",
"points_due_at": null,
"points_given_at": "2024-06-01T12:05:00.000Z",
"points_expiry_at": null,
"created_at": "2024-06-01T12:00:00.000Z",
"updated_at": "2024-06-01T12:05:00.000Z"
}
},
"meta": {
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Earning history entry 'entry_abc' not found"
}
]
}
```
# Get reward history entry
Source: https://developers.mageloyalty.com/api-reference/customers/get-reward-history
GET /v1/customers/{identifier}/reward-history/{id}
Get a single reward redemption entry.
Get a single reward history entry by its ID. Validates that the entry belongs to the given customer and shop.
## Response
Returns a single [reward history entry](/api-reference/customers/list-reward-history) object. The response `meta` includes `loyalty_mode` and `currency` so you can read the `points_spent` value correctly (see [Loyalty modes](/api-reference/loyalty-modes)).
## Errors
| Status | Scenario |
| - | - |
| `404` | Entry or customer not found |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345/reward-history/cr_abc123 \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345/reward-history/cr_abc123",
{
method: "GET",
headers: { Authorization: "" },
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers/6789012345/reward-history/cr_abc123",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => ["Authorization: "],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "reward_history_entry",
"id": "cr_abc123",
"attributes": {
"reward_id": "reward_abc",
"reward_name": "£5 off your next order",
"reward_discount_type": "fixed",
"points_spent": 500,
"discount_code": "MAGE-ABC123",
"status": "active",
"is_refunded": false,
"discount_code_redeemed_at": null,
"redeemed_at": "2024-06-01T12:00:00.000Z",
"expires_at": "2024-07-01T12:00:00.000Z",
"created_at": "2024-06-01T12:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
}
},
"meta": {
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Reward history entry 'cr_abc123' not found"
}
]
}
```
# List customers
Source: https://developers.mageloyalty.com/api-reference/customers/list
GET /v1/customers
List all loyalty program customers.
List all loyalty customers for the shop.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `filter[email]` | string | Partial, case-insensitive email match |
| `filter[loyalty_status]` | string | Filter by `member` or `excluded` |
| `sort` | string | Sort field. Prefix with `-` for descending. Options: `created_at`, `points`, `lifetime_points`, `redeemed_points` |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Response
Returns a paginated list of [customer objects](/api-reference/customers/object) with a `meta` object describing the page.
```bash cURL theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/customers?page[size]=20&sort=-lifetime_points' \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers?page[size]=20&sort=-lifetime_points",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers",
headers={"Authorization": ""},
params={"page[size]": 20, "sort": "-lifetime_points"},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Gold",
"threshold": 500,
"points_multiplier": 1.5,
"badge_image_url": null,
"badge_color": "#FFD700"
}, "created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z"
}
}
],
"meta": {
"total": 1,
"page": 1,
"per_page": 20,
"total_pages": 1,
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
# List earning history
Source: https://developers.mageloyalty.com/api-reference/customers/list-earning-history
GET /v1/customers/{identifier}/earning-history
List a customer's point earning history.
List a customer's point earning history. The `identifier` can be a Shopify customer ID or email.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `filter[status]` | string | Filter by status: `pending`, `approved`, `rejected` |
| `sort` | string | Sort field. Prefix with `-` for descending. Options: `created_at`, `points`, `points_given_at`. Default: `-created_at` |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Response
Returns a `data` array of `earning_history_entry` objects alongside a paginated `meta`. Each entry has these attributes:
| Field | Type | Description |
| - | - | - |
| `earning_rule_id` | string \| null | The earning rule that produced this entry |
| `action` | string \| null | The rule action, for example `purchase` or `birthday` |
| `points` | integer | Points awarded. Minor currency units in store-credit mode (see [Loyalty modes](/api-reference/loyalty-modes)) |
| `base_points` | integer \| null | Points before tier and campaign multipliers |
| `points_multiplier` | number \| null | The combined multiplier applied |
| `status` | string | `pending`, `approved`, or `rejected` |
| `order_id` | string \| null | The originating Shopify order ID, if any |
| `order_number` | string \| null | The originating order number, if any |
| `points_due_at` | string \| null | When pending points become available |
| `points_given_at` | string \| null | When the points were awarded |
| `points_expiry_at` | string \| null | When the points expire, if applicable |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |
The `meta` object includes `loyalty_mode` and `currency` so you can read the `points` values correctly.
## Errors
| Status | Scenario |
| - | - |
| `404` | Customer not found |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345/earning-history \
--header 'Authorization: '
```
```bash cURL (filter + sort + page) theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/customers/6789012345/earning-history?filter[status]=approved&sort=-points_given_at&page[number]=1&page[size]=20' \
--header 'Authorization: '
```
```javascript Node theme={null}
const params = new URLSearchParams({
"filter[status]": "approved",
sort: "-points_given_at",
"page[number]": "1",
"page[size]": "20",
});
const response = await fetch(
`https://api.mageloyalty.com/v1/customers/6789012345/earning-history?${params}`,
{
method: "GET",
headers: { Authorization: "" },
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers/6789012345/earning-history",
headers={"Authorization": ""},
params={
"filter[status]": "approved",
"sort": "-points_given_at",
"page[number]": 1,
"page[size]": 20,
},
)
data = response.json()
```
```php PHP theme={null}
"approved",
"sort" => "-points_given_at",
"page[number]" => 1,
"page[size]" => 20,
]);
$ch = curl_init("https://api.mageloyalty.com/v1/customers/6789012345/earning-history?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: "],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "earning_history_entry",
"id": "entry_abc",
"attributes": {
"earning_rule_id": "rule_abc",
"action": "purchase",
"points": 150,
"base_points": 100,
"points_multiplier": 1.5,
"status": "approved",
"order_id": "5678901234",
"order_number": "#1042",
"points_due_at": null,
"points_given_at": "2024-06-01T12:05:00.000Z",
"points_expiry_at": null,
"created_at": "2024-06-01T12:00:00.000Z",
"updated_at": "2024-06-01T12:05:00.000Z"
}
}
],
"meta": {
"total": 12,
"page": 1,
"per_page": 20,
"total_pages": 1,
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# List reward history
Source: https://developers.mageloyalty.com/api-reference/customers/list-reward-history
GET /v1/customers/{identifier}/reward-history
List a customer's reward redemption history.
List a customer's redeemed reward history. The `identifier` can be a Shopify customer ID or email.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `filter[status]` | string | Filter by status: `active`, `used`, `expired` |
| `sort` | string | Sort field. Prefix with `-` for descending. Options: `created_at`, `points_spent`. Default: `-created_at` |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Response
`redeemed_at` is the moment the customer redeemed (claimed) the reward and spent their points, which is also the entry's creation time. `discount_code_redeemed_at` is the separate, later moment when the customer actually used the discount code at checkout (it stays `null` until then).
`points_spent` is a minor-unit amount on store-credit shops. The `meta` object includes `loyalty_mode` and `currency` so you can read it correctly (see [Loyalty modes](/api-reference/loyalty-modes)).
## Errors
| Status | Scenario |
| - | - |
| `404` | Customer not found |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/customers/6789012345/reward-history \
--header 'Authorization: '
```
```bash cURL (filter + sort + page) theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/customers/6789012345/reward-history?filter[status]=active&sort=-created_at&page[number]=1&page[size]=20' \
--header 'Authorization: '
```
```javascript Node theme={null}
const params = new URLSearchParams({
"filter[status]": "active",
sort: "-created_at",
"page[number]": "1",
"page[size]": "20",
});
const response = await fetch(
`https://api.mageloyalty.com/v1/customers/6789012345/reward-history?${params}`,
{
method: "GET",
headers: { Authorization: "" },
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers/6789012345/reward-history",
headers={"Authorization": ""},
params={
"filter[status]": "active",
"sort": "-created_at",
"page[number]": 1,
"page[size]": 20,
},
)
data = response.json()
```
```php PHP theme={null}
"active",
"sort" => "-created_at",
"page[number]" => 1,
"page[size]" => 20,
]);
$ch = curl_init("https://api.mageloyalty.com/v1/customers/6789012345/reward-history?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: "],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "reward_history_entry",
"id": "cr_abc123",
"attributes": {
"reward_id": "reward_abc",
"reward_name": "\u00a35 off your next order",
"reward_discount_type": "fixed",
"points_spent": 500,
"discount_code": "MAGE-ABC123",
"status": "active",
"is_refunded": false,
"discount_code_redeemed_at": null,
"redeemed_at": "2024-06-01T12:00:00.000Z",
"expires_at": "2024-07-01T12:00:00.000Z",
"created_at": "2024-06-01T12:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
}
}
],
"meta": {
"total": 5,
"page": 1,
"per_page": 20,
"total_pages": 1,
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# Customer object
Source: https://developers.mageloyalty.com/api-reference/customers/object
The customer object represents a loyalty program member.
The customer object represents a member of your loyalty program. Customers are identified by their Shopify customer ID or email address.
## Attributes
| Field | Type | Description |
| - | - | - |
| `shopify_customer_id` | string | The customer's Shopify customer ID |
| `email` | string | Email address |
| `first_name` | string | First name |
| `last_name` | string | Last name |
| `current_points` | integer | Current available points balance. Minor currency units in store-credit mode (see [Loyalty modes](/api-reference/loyalty-modes)) |
| `lifetime_points` | integer | Total points earned over the lifetime of the account |
| `redeemed_points` | integer | Total points spent on rewards |
| `loyalty_status` | string | Either `member` or `excluded` |
| `loyalty_mode` | string | Either `points` or `store_credit` |
| `store_credit` | object \| null | Pre-converted balance in store-credit mode, or `null` in points mode (see below) |
| `date_of_birth` | string \| null | ISO 8601 date, or `null` if not set |
| `referral_url` | string \| null | The customer's unique referral link, or `null` if not generated |
| `referral_code` | string \| null | The customer's referral code, or `null` if not generated |
| `vip_tier` | object \| null | The customer's current VIP tier (see below) |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |
### Embedded VIP tier
When present, the `vip_tier` object contains:
| Field | Type | Description |
| - | - | - |
| `id` | string | Tier ID |
| `name` | string | Tier name |
| `threshold` | integer | Lifetime points required to reach this tier |
| `points_multiplier` | number | Points earning multiplier for this tier |
| `badge_image_url` | string \| null | URL to the tier badge image |
| `badge_color` | string \| null | Hex color code for the tier badge |
### Store credit
On store-credit shops, `store_credit` holds the balance pre-converted to money so you do not have to do the minor-unit math. It is `null` in points mode. See [Loyalty modes](/api-reference/loyalty-modes).
| Field | Type | Description |
| - | - | - |
| `balance_minor` | integer | The balance in minor units (identical to `current_points`) |
| `balance` | string | The balance in major units as a decimal string, for example `"4.50"` |
| `balance_formatted` | string | A localized display string, for example `"£4.50"` |
| `currency` | string | The ISO 4217 currency code |
## Example
```json theme={null}
{
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Gold",
"threshold": 500,
"points_multiplier": 1.5,
"badge_image_url": null,
"badge_color": "#FFD700"
},
"created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z"
}
}
```
### Example (store-credit shop)
On a store-credit shop the points fields are minor units, `loyalty_mode` is `store_credit`, and `store_credit` is populated:
```json theme={null}
{
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "store_credit",
"store_credit": {
"balance_minor": 450,
"balance": "4.50",
"balance_formatted": "£4.50",
"currency": "GBP"
},
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Gold",
"threshold": 500,
"points_multiplier": 1.5,
"badge_image_url": null,
"badge_color": "#FFD700"
},
"created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2024-06-20T14:30:00.000Z"
}
}
```
# Update a customer
Source: https://developers.mageloyalty.com/api-reference/customers/update
PUT /v1/customers/{identifier}
Update editable fields on a customer.
Update editable fields on a customer. The `identifier` can be a Shopify customer ID or email.
The request body may be sent in either JSON:API format (attributes nested under `data.attributes`) or as a flat object. Both forms accept the same field names.
Only the fields listed below are updatable. Any other field in the body is ignored. At least one updatable field must be present, otherwise the request returns `400`.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `exclude_vip` | `true` | When set to `true`, omits the VIP tier lookup and returns `vip_tier` as `null` |
## Request body
| Field | Type | Description |
| - | - | - |
| `date_of_birth` | string \| null | ISO 8601 date. Pass `null` to clear. |
| `is_excluded` | boolean | Whether the customer is excluded from the loyalty program |
| `anniversary_date` | string \| null | ISO 8601 date. Pass `null` to clear. |
**JSON:API format:**
```json theme={null}
{
"data": {
"attributes": {
"date_of_birth": "1990-05-15T00:00:00.000Z",
"is_excluded": false,
"anniversary_date": "2023-03-10T09:00:00.000Z"
}
}
}
```
**Flat format:**
```json theme={null}
{
"date_of_birth": "1990-05-15T00:00:00.000Z",
"is_excluded": false,
"anniversary_date": "2023-03-10T09:00:00.000Z"
}
```
## Response
Returns the updated [customer object](/api-reference/customers/object).
## Errors
| Status | Scenario |
| - | - |
| `400` | Invalid JSON body, or no updatable fields provided |
| `404` | Customer not found |
```bash cURL theme={null}
# JSON:API format
curl --request PUT \
--url https://api.mageloyalty.com/v1/customers/6789012345 \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"data": {
"attributes": {
"date_of_birth": "1990-05-15T00:00:00.000Z",
"is_excluded": false,
"anniversary_date": "2023-03-10T09:00:00.000Z"
}
}
}'
# Flat format
curl --request PUT \
--url https://api.mageloyalty.com/v1/customers/6789012345 \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"date_of_birth": "1990-05-15T00:00:00.000Z",
"is_excluded": false,
"anniversary_date": "2023-03-10T09:00:00.000Z"
}'
```
```javascript Node theme={null}
// JSON:API format
const response = await fetch(
"https://api.mageloyalty.com/v1/customers/6789012345",
{
method: "PUT",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
attributes: {
date_of_birth: "1990-05-15T00:00:00.000Z",
is_excluded: false,
anniversary_date: "2023-03-10T09:00:00.000Z",
},
},
}),
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
# Flat format
response = requests.put(
"https://api.mageloyalty.com/v1/customers/6789012345",
headers={"Authorization": ""},
json={
"date_of_birth": "1990-05-15T00:00:00.000Z",
"is_excluded": False,
"anniversary_date": "2023-03-10T09:00:00.000Z",
},
)
data = response.json()
```
```php PHP theme={null}
"PUT",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"date_of_birth" => "1990-05-15T00:00:00.000Z",
"is_excluded" => false,
"anniversary_date" => "2023-03-10T09:00:00.000Z",
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "customer",
"id": "6789012345",
"attributes": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"current_points": 450,
"lifetime_points": 1200,
"redeemed_points": 750,
"loyalty_status": "member",
"loyalty_mode": "points",
"store_credit": null,
"date_of_birth": "1990-05-15T00:00:00.000Z",
"referral_url": "https://mystore.myshopify.com?referral_code=ABC123",
"referral_code": "ABC123",
"vip_tier": {
"id": "tier_abc",
"name": "Gold",
"threshold": 500,
"points_multiplier": 1.5,
"badge_image_url": null,
"badge_color": "#FFD700"
}, "created_at": "2024-01-15T10:00:00.000Z",
"updated_at": "2026-06-23T12:00:00.000Z"
}
}
}
```
```json 400 Bad Request theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "No updatable fields provided"
}
]
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
# Award an earning rule
Source: https://developers.mageloyalty.com/api-reference/earning-rules/award
POST /v1/earning-rules/{id}/award
Award points to a customer for completing an earning rule.
Award points to a customer for completing an earning rule action, using the rule's configured points and any applicable VIP tier multiplier.
Only `social` and `engagement` category rules can be awarded via the API. Rules in other categories (e.g. purchase, birthday, anniversary) are managed automatically by the platform.
Each rule can only be awarded **once per customer** — duplicate requests return `409 Conflict`.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `customer_identifier` | string | Yes | Shopify customer ID or email address |
## Validations
* Customer must exist and not be excluded from the loyalty program
* Earning rule must be active
* Earning rule category must be `social` or `engagement`
* Customer must not have already been awarded points for this rule
## Response
Returns `201 Created`.
| Field | Type | Description |
| - | - | - |
| `earning_rule_id` | string \| null | The earning rule that was awarded |
| `action` | string \| null | The rule's action type |
| `points` | integer | Final points awarded after the multiplier |
| `base_points` | integer \| null | Points before any multiplier |
| `points_multiplier` | number \| null | The applied VIP tier multiplier (`null` when no tier boost applied) |
| `bonus_campaign_multiplier` | number \| null | The bonus campaign multiplier applied, or `null` |
| `points_balance` | integer \| null | The customer's balance after the award. Minor currency units in store-credit mode |
| `loyalty_mode` | string | Either `points` or `store_credit` |
| `currency` | string | The shop's ISO 4217 currency code |
| `status` | string | `approved` or `pending` |
| `points_given_at` | string \| null | When points were awarded, or `null` while pending |
| `points_due_at` | string \| null | When pending points will be approved |
| `points_expiry_at` | string \| null | When the points expire, or `null` |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |
On store-credit shops the response also includes `credit_earned` and `credit_balance` companions with the major-unit decimal strings. These are absent in points mode. See [Loyalty modes](/api-reference/loyalty-modes).
When the rule has an approval delay configured, `status` will be `pending`, `points_given_at` will be `null`, and `points_due_at` will reflect when the points will be approved.
## Errors
| Status | Scenario |
| - | - |
| `400` | `customer_identifier` missing or invalid JSON |
| `403` | Customer is excluded from the loyalty program |
| `404` | Customer or earning rule not found (or rule is inactive) |
| `409` | Customer has already been awarded points for this earning rule |
| `422` | Earning rule category cannot be awarded via the API |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/earning-rules/rule_abc/award \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"customer_identifier": "6789012345"
}'
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/earning-rules/rule_abc/award",
{
method: "POST",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_identifier: "6789012345",
}),
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/earning-rules/rule_abc/award",
headers={"Authorization": ""},
json={
"customer_identifier": "6789012345",
},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"customer_identifier" => "6789012345",
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 201 Created theme={null}
{
"data": {
"type": "earning_history_entry",
"id": "cer_abc123",
"attributes": {
"earning_rule_id": "rule_abc",
"action": "follow_on_instagram",
"points": 150,
"base_points": 100,
"points_multiplier": 1.5,
"bonus_campaign_multiplier": null,
"points_balance": 1350,
"loyalty_mode": "points",
"currency": "GBP",
"status": "approved",
"points_given_at": "2024-06-01T12:00:00.000Z",
"points_due_at": null,
"points_expiry_at": null,
"created_at": "2024-06-01T12:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
}
}
}
```
```json 201 Created (store-credit shop) theme={null}
{
"data": {
"type": "earning_history_entry",
"id": "cer_abc123",
"attributes": {
"earning_rule_id": "rule_abc",
"action": "follow_on_instagram",
"points": 150,
"base_points": 100,
"points_multiplier": 1.5,
"bonus_campaign_multiplier": null,
"points_balance": 1350,
"loyalty_mode": "store_credit",
"currency": "GBP",
"credit_earned": "1.50",
"credit_balance": "13.50",
"status": "approved",
"points_given_at": "2024-06-01T12:00:00.000Z",
"points_due_at": null,
"points_expiry_at": null,
"created_at": "2024-06-01T12:00:00.000Z",
"updated_at": "2024-06-01T12:00:00.000Z"
}
}
}
```
```json 403 Forbidden theme={null}
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "Customer is excluded from the loyalty program"
}
]
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
```json 409 Conflict theme={null}
{
"errors": [
{
"status": "409",
"title": "Conflict",
"detail": "Customer has already been awarded points for this earning rule"
}
]
}
```
```json 422 Unprocessable theme={null}
{
"errors": [
{
"status": "422",
"title": "Unprocessable",
"detail": "Earning rule category 'purchase' cannot be awarded via the API. Only 'social' and 'engagement' rules are supported."
}
]
}
```
# Award a custom action
Source: https://developers.mageloyalty.com/api-reference/earning-rules/award-custom-action
POST /v1/actions
Record a custom action completion and award points to a customer from your own systems.
Custom actions let you award points for things that happen outside Shopify — a mobile app install, an in-app purchase, an event check-in, a completed survey, or any milestone your own systems track.
You create a custom action once in the Mage admin (**Loyalty Program → Earning rules → Advanced → Custom Action**), give it a stable **action key**, then call this endpoint from your backend whenever the action happens. You can also trigger it with no code using the [Shopify Flow](#trigger-from-shopify-flow) action.
Custom actions are available on the **Growth** plan and above. The customer must already be a loyalty member — a call for someone who is not enrolled returns `404`.
Points come from the rule's configured amount, with any applicable VIP tier and bonus campaign multipliers applied automatically. Each rule is either **once per customer** or **unlimited**, depending on how you configured it.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `action_key` | string | Yes | The action key of the custom rule. Lowercase letters, numbers, and underscores only |
| `customer_id` | string | Conditional | Shopify customer ID (numeric). Required if `customer_email` is not provided |
| `customer_email` | string | Conditional | Customer email. Required if `customer_id` is not provided |
| `idempotency_key` | string | No | A unique value for this specific completion. A repeat with the same key returns the original award instead of granting again |
| `metadata` | object | No | Free-form context stored alongside the earning record |
## Idempotency
If you pass an `idempotency_key`, a repeated request with the same key for the same rule returns the original award with `200 OK` and `replayed: true` — points are never granted twice. For an unlimited rule where each completion should count once, set the key to a stable id for that completion, such as an event id or order id.
An **unlimited** rule awards on every call. Without an `idempotency_key` there is nothing to deduplicate on, so any retry — a network failure, an app relaunch, a repeated event — awards again and double-grants points. Always send a stable `idempotency_key` per real completion on unlimited rules.
**Once-per-customer** rules are protected regardless: a second award returns `409`, with or without a key.
## Validations
* Customer must exist and not be excluded from the loyalty program
* A custom action with the given `action_key` must exist and be active
* For a once-per-customer rule, the customer must not have already been awarded this action
## Response
Returns `201 Created` on success, or `200 OK` for an idempotent replay. The `points` and `points_balance` fields are minor currency units on store-credit shops, and the response carries `loyalty_mode` and `currency`. On store-credit shops it also includes `credit_earned` and `credit_balance` companions (major-unit decimal strings), which are absent in points mode. See [Loyalty modes](/api-reference/loyalty-modes).
When the rule has an approval delay configured, `status` is `pending`, `points_given_at` is `null`, `points_due_at` reflects when the points will be approved, and `points_balance` excludes the pending points until then.
If the loyalty program is disabled, or test mode is filtering this customer, the request still succeeds with `200 OK` but no points are awarded:
```json theme={null}
{
"data": null,
"meta": { "awarded": false, "reason": "program_disabled" }
}
```
## Errors
| Status | Scenario |
| - | - |
| `400` | `action_key` missing or invalid, neither `customer_id` nor `customer_email` provided, or invalid JSON |
| `403` | Customer is excluded from the loyalty program |
| `404` | No active custom action matches the `action_key`, or the customer is not a loyalty member |
| `409` | A once-per-customer rule has already been awarded to this customer |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
**Example `409` response:**
```json theme={null}
{
"errors": [
{
"status": "409",
"title": "Conflict",
"detail": "Customer has already been awarded points for this action. The action is configured to award once per customer."
}
]
}
```
## Trigger from Shopify Flow
If you would rather not write code, add the **Award Custom Action** action to any Shopify Flow workflow and set:
* **Action key** — the same key from the rule's setup page
* **Customer** — map a customer from the workflow's trigger
* **Idempotency key** (optional) — leave blank to use the Flow run id automatically, so a retried run never awards twice
Shopify Flow signs and sends the request for you, so no API key is needed for this path.
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/actions \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"action_key": "mobile_app_install",
"customer_id": "6789012345",
"idempotency_key": "install-6789012345-ios"
}'
```
```javascript Node theme={null}
const response = await fetch("https://api.mageloyalty.com/v1/actions", {
method: "POST",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
action_key: "mobile_app_install",
customer_id: "6789012345",
idempotency_key: "install-6789012345-ios",
}),
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/actions",
headers={"Authorization": ""},
json={
"action_key": "mobile_app_install",
"customer_id": "6789012345",
"idempotency_key": "install-6789012345-ios",
},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"action_key" => "mobile_app_install",
"customer_id" => "6789012345",
"idempotency_key" => "install-6789012345-ios",
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 201 Awarded theme={null}
{
"data": {
"type": "earning_history_entry",
"id": "cer_abc123",
"attributes": {
"earning_rule_id": "rule_abc",
"action": "custom",
"points": 100,
"points_balance": 1200,
"loyalty_mode": "points",
"currency": "GBP",
"status": "approved",
"idempotency_key": "install-6789012345-ios",
"replayed": false,
"points_given_at": "2026-06-23T12:00:00.000Z",
"points_due_at": null,
"created_at": "2026-06-23T12:00:00.000Z"
}
}
}
```
```json 200 Replay theme={null}
{
"data": {
"type": "earning_history_entry",
"id": "cer_abc123",
"attributes": {
"earning_rule_id": "rule_abc",
"action": "custom",
"points": 100,
"points_balance": 1200,
"loyalty_mode": "points",
"currency": "GBP",
"status": "approved",
"idempotency_key": "install-6789012345-ios",
"replayed": true,
"points_given_at": "2026-06-23T12:00:00.000Z",
"points_due_at": null,
"created_at": "2026-06-23T12:00:00.000Z"
}
}
}
```
```json 409 Conflict theme={null}
{
"errors": [
{
"status": "409",
"title": "Conflict",
"detail": "Customer has already been awarded points for this action. The action is configured to award once per customer."
}
]
}
```
# List earning rules
Source: https://developers.mageloyalty.com/api-reference/earning-rules/list
GET /v1/earning-rules
List all earning rules for the shop.
List all earning rules for the shop.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `filter[is_active]` | string | Filter by active status: `true` or `false` |
| `filter[customer_identifier]` | string | A Shopify customer ID or email. When supplied, the customer's VIP tier multiplier is applied to each rule's calculated `points`, and the `tier_info` meta object is populated |
| `sort` | string | Sort field. Prefix with `-` for descending. Options: `created_at`, `name`. Default: `-created_at` |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Calculated points
Each rule returns pre-calculated points fields alongside its configuration:
* `points` — final points with any applicable VIP tier and bonus campaign multipliers applied
* `base_points` — the rule's base points before multipliers
* `tier_boost` — the VIP tier multiplier applied, or `null`
* `campaign_boost` — the active bonus campaign multiplier applied, or `null`
Active bonus campaign boosts are reflected for every request. VIP tier boosts are only applied when `filter[customer_identifier]` is supplied and that customer holds a qualifying tier.
## Response meta
In addition to pagination, the top-level `meta` object includes:
| Field | Type | Description |
| - | - | - |
| `loyalty_mode` | string | Either `points` or `store_credit`. Point values are minor currency units in store-credit mode (see [Loyalty modes](/api-reference/loyalty-modes)) |
| `currency` | string | The shop's ISO 4217 currency code |
| `tier_info` | object \| null | Present (non-null) only when `filter[customer_identifier]` resolves a customer with a qualifying VIP tier. Shape: `{ multiplier, name, mode }` |
| `campaign_info` | object \| null | The active bonus campaign, or `null` when none is running. Shape: `{ name }` |
```bash cURL theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/earning-rules?filter[customer_identifier]=6789012345' \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/earning-rules?filter[customer_identifier]=6789012345",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/earning-rules",
headers={"Authorization": ""},
params={"filter[customer_identifier]": "6789012345"},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "earning_rule",
"id": "rule_abc",
"attributes": {
"name": "Points for purchase",
"description": null,
"action": "purchase",
"category": "purchase",
"is_active": true,
"points_to_give": 10,
"customer_facing_label": "Earn 10 points per £1 spent",
"call_to_action_url": null,
"created_at": "2024-01-01T00:00:00.000Z",
"points": 15,
"base_points": 10,
"tier_boost": 1.5,
"campaign_boost": null
}
},
{
"type": "earning_rule",
"id": "rule_def",
"attributes": {
"name": "Follow on Instagram",
"description": null,
"action": "follow_on_instagram",
"category": "social",
"is_active": true,
"points_to_give": 100,
"customer_facing_label": "Earn 100 points",
"call_to_action_url": "https://instagram.com/yourbrand",
"created_at": "2024-01-01T00:00:00.000Z",
"points": 150,
"base_points": 100,
"tier_boost": 1.5,
"campaign_boost": null
}
}
],
"meta": {
"total": 5,
"page": 1,
"per_page": 20,
"total_pages": 1,
"loyalty_mode": "points",
"currency": "GBP",
"tier_info": {
"multiplier": 1.5,
"name": "Gold",
"mode": "all"
},
"campaign_info": null
}
}
```
```json 401 Unauthorized theme={null}
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "Invalid or revoked API key"
}
]
}
```
# Earning rule object
Source: https://developers.mageloyalty.com/api-reference/earning-rules/object
The earning rule object defines how customers earn points.
Earning rules define the actions customers can take to earn points. Rules are categorized by type — purchase, social, engagement, birthday, anniversary, etc.
## Attributes
| Field | Type | Description |
| - | - | - |
| `name` | string | Rule display name |
| `description` | string \| null | Optional description |
| `action` | string | The action type (e.g. `purchase`, `follow_on_instagram`, `signup`, `birthday`) |
| `category` | string | Rule category: `purchase`, `social`, `engagement`, `birthday`, `anniversary`, `signup`, `referral` |
| `is_active` | boolean | Whether the rule is currently active |
| `points_to_give` | integer | Base points awarded when the rule is completed. Minor currency units in store-credit mode (see [Loyalty modes](/api-reference/loyalty-modes)) |
| `customer_facing_label` | string \| null | Label shown to customers (e.g. "Earn 10 points per £1 spent") |
| `call_to_action_url` | string \| null | URL for the action (e.g. Instagram profile link) |
| `created_at` | string | ISO 8601 timestamp |
| `points` | integer | Final points awarded, pre-calculated with any applicable VIP tier and bonus campaign multipliers applied |
| `base_points` | integer | The rule's base points before any multipliers |
| `tier_boost` | number \| null | The VIP tier multiplier applied to `points`, or `null` when no tier boost applies |
| `campaign_boost` | number \| null | The bonus campaign multiplier applied to `points`, or `null` when no active campaign boosts this rule |
`points` is pre-calculated: it reflects `base_points` with the VIP tier and bonus campaign multipliers already applied where they qualify. Tier boosts only appear when a customer identifier is supplied (see [List earning rules](/api-reference/earning-rules/list)).
## Example
```json theme={null}
{
"type": "earning_rule",
"id": "rule_abc",
"attributes": {
"name": "Points for purchase",
"description": null,
"action": "purchase",
"category": "purchase",
"is_active": true,
"points_to_give": 10,
"customer_facing_label": "Earn 10 points per £1 spent",
"call_to_action_url": null,
"created_at": "2024-01-01T00:00:00.000Z",
"points": 10,
"base_points": 10,
"tier_boost": null,
"campaign_boost": null
}
}
```
# Errors
Source: https://developers.mageloyalty.com/api-reference/errors
Error response codes and format used by the Mage Loyalty API.
## Error format
The Mage Loyalty API uses conventional HTTP status codes to indicate the success or failure of a request. Codes in the `2xx` range indicate success, codes in the `4xx` range indicate a client error, and codes in the `5xx` range indicate a server-side problem.
All errors return a JSON body following the [JSON:API error format](https://jsonapi.org/format/#errors):
```json theme={null}
{
"errors": [
{
"status": "422",
"title": "Insufficient Points",
"detail": "Customer has 200 points but this reward costs 500"
}
]
}
```
## Error response codes
| Status | Meaning |
| - | - |
| `400` | **Bad Request** — Missing or invalid fields in the request body |
| `401` | **Unauthorized** — Missing, invalid, or revoked API key |
| `403` | **Forbidden** — Read-only API key attempted a write operation, customer is excluded from the loyalty program, or customer is not on the required VIP tier |
| `404` | **Not Found** — The requested resource does not exist for this shop |
| `405` | **Method Not Allowed** — The HTTP method is not supported on this endpoint |
| `409` | **Conflict** — Duplicate action, e.g. awarding an earning rule already completed by this customer |
| `422` | **Unprocessable Entity** — The request is well-formed but cannot be completed, e.g. insufficient points |
| `429` | **Too Many Requests** — Rate limit exceeded |
| `500` | **Internal Server Error** — Something went wrong on our end |
| `503` | **Service Unavailable** — Unable to connect to Shopify |
The `status` field is always returned as a **string** (for example `"404"`), not a number.
## Example error bodies
A request for a customer that does not exist returns `404 Not Found`:
```json theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
A request body that is not valid JSON returns `400 Bad Request`:
```json theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "Invalid JSON body"
}
]
}
```
# Introduction
Source: https://developers.mageloyalty.com/api-reference/introduction
The Mage Loyalty REST API lets you programmatically manage your loyalty program.
The Mage Loyalty API is organized around REST. It has predictable, resource-oriented URLs, uses standard HTTP methods, and returns JSON.
All requests go to the versioned base URL. The current version is `v1`:
```
https://api.mageloyalty.com/v1
```
## Authentication
Every request must include an API key in the `Authorization` header. You create and manage keys in the Mage Loyalty dashboard under **Settings > API Keys** (available on the Growth plan and above), and you can issue full-access or read-only keys.
See [Authentication](/api-reference/authentication) to create your first key and start making requests.
## Response format
Responses use a JSON:API-inspired shape. A single resource is wrapped in a `data` object with `type`, `id`, and `attributes`:
```json theme={null}
{
"data": {
"type": "customer",
"id": "6789012345",
"attributes": {
"email": "jane@example.com",
"current_points": 450
}
}
}
```
List endpoints return a `data` array alongside a `meta` object (see [Pagination](/api-reference/pagination)), and failed requests return an `errors` array (see [Errors](/api-reference/errors)).
## Points and store credit
A shop runs its loyalty program on either points or store credit. The API is the same in both modes, but in store-credit mode every points-named value (balances, `points_cost`, `adjustment`, tier thresholds) is expressed in the currency's minor units. Responses that carry a loyalty value include a `loyalty_mode` and `currency` so you can read them correctly. See [Loyalty modes](/api-reference/loyalty-modes) for the full picture.
## Rate limits
The API allows 6 requests per second per API key. See [Rate limits](/api-reference/rate-limits) for the response headers and retry guidance.
## Available resources
Manage loyalty program members, points, and history
Configure how customers earn points
Manage available rewards and redemptions
View tier configurations and requirements
# Loyalty modes
Source: https://developers.mageloyalty.com/api-reference/loyalty-modes
How the API represents points and store credit, and how to read loyalty values in each mode.
Every shop runs its loyalty program in one of two modes, set by the merchant:
* **`points`** The loyalty balance is plain points.
* **`store_credit`** The loyalty balance is Mage store credit, denominated in money.
The API surface is identical in both modes. Every endpoint returns the same fields either way. What changes is how you interpret the numbers, and, for reward redemption, which flow is available.
This is Mage's internal store credit, not Shopify native store credit. The loyalty engine treats both modes the same way, so points and store-credit shops share one API.
## How values are denominated
In `points` mode, every points field is a whole number of points. In `store_credit` mode, the same fields hold the balance in the currency's **minor units**. For most currencies that means cents, so `450` means 4.50 in the shop currency. For zero-decimal currencies such as JPY and KRW the factor is 1, so `450` means 450 yen.
This applies to every points-named field the API returns or accepts, including `current_points`, `lifetime_points`, `redeemed_points`, a reward's `points_cost`, an earning rule's `points`, the `adjustment` on a points adjustment, and a tier's `threshold`.
| | `points` mode | `store_credit` mode |
| - | - | - |
| Unit | Whole points | Currency minor units |
| `current_points: 450` means | 450 points | 4.50 in the shop currency (450 for JPY/KRW) |
| Input values (e.g. `adjustment`) | Whole points | Minor units |
| Reward redemption | Standard or flexible | Flexible only |
Inputs are always minor units in store-credit mode. To grant a customer 5.00 of credit on a GBP shop, send `adjustment: 500`, not `adjustment: 5`.
## Detecting the mode
Responses that carry a loyalty value tell you the mode and currency so you can format correctly. Single-resource and list responses include `loyalty_mode` and `currency` in their `meta`, and the customer object carries `loyalty_mode` in its attributes.
```json theme={null}
{
"data": { "type": "customer", "id": "6789012345", "attributes": { "...": "..." } },
"meta": {
"loyalty_mode": "store_credit",
"currency": "GBP"
}
}
```
`loyalty_mode` is always one of `points` or `store_credit`. `currency` is the shop's ISO 4217 code (for example `GBP`), and defaults to `USD` when the shop has not set one.
## The store\_credit object
On store-credit shops, the customer object includes a `store_credit` object with pre-converted representations of the balance, so you do not have to do the minor-unit math yourself. In points mode it is `null`.
| Field | Type | Description |
| - | - | - |
| `balance_minor` | integer | The balance in minor units (identical to `current_points`) |
| `balance` | string | The balance in major units as a decimal string, for example `"4.50"` |
| `balance_formatted` | string | A localized display string, for example `"£4.50"` |
| `currency` | string | The ISO 4217 currency code |
```json theme={null}
"store_credit": {
"balance_minor": 450,
"balance": "4.50",
"balance_formatted": "£4.50",
"currency": "GBP"
}
```
## Credit companions on write responses
Endpoints that report a change to a customer's balance (adjusting points, awarding an earning rule, recording a custom action, purchasing a reward) return `credit_*` companion fields alongside the raw minor-unit numbers. Each companion is the major-unit value as a decimal string. Companions appear **only** in store-credit mode; in points mode these fields are absent.
For example, a points adjustment of `500` on a GBP store-credit shop returns:
```json theme={null}
{
"adjustment": 500,
"points_before": 200,
"points_after": 700,
"current_points": 700,
"loyalty_mode": "store_credit",
"currency": "GBP",
"credit_adjustment": "5.00",
"credit_before": "2.00",
"credit_after": "7.00",
"credit_balance": "7.00"
}
```
The specific companion names for each endpoint are listed on that endpoint's page. Webhook events use the same pattern (see [Webhooks](/api-reference/webhooks/getting-started)).
## Redeeming rewards
In `points` mode a reward is redeemed with [Purchase a reward](/api-reference/rewards/purchase), whether it is a standard fixed-cost reward or a flexible one.
In `store_credit` mode, only **flexible** rewards can be redeemed. A standard fixed-cost reward returns `403 Forbidden` with the detail `This reward cannot be redeemed in store credit mode`. A flexible reward lets the customer choose how much credit to spend, and the server pins the conversion at 1 minor unit of credit to 1 minor unit of discount. Read a reward's `redemption_mode` and `flexible_config` from [List rewards](/api-reference/rewards/list) to know which flow applies and what range is valid.
## Endpoints that carry mode context
These responses include `loyalty_mode` and `currency`:
* [Get a customer](/api-reference/customers/get), [List customers](/api-reference/customers/list), [Enroll a customer](/api-reference/customers/enroll), [Update a customer](/api-reference/customers/update) (customer object plus list `meta`)
* [Adjust points](/api-reference/customers/adjust-points)
* [List](/api-reference/customers/list-earning-history) and [get earning history](/api-reference/customers/get-earning-history)
* [List](/api-reference/customers/list-reward-history) and [get reward history](/api-reference/customers/get-reward-history)
* [List earning rules](/api-reference/earning-rules/list), [Award an earning rule](/api-reference/earning-rules/award), [Record a custom action](/api-reference/earning-rules/award-custom-action)
* [List rewards](/api-reference/rewards/list), [Purchase a reward](/api-reference/rewards/purchase)
* [List VIP tiers](/api-reference/vip-tiers/list), [Get a VIP tier](/api-reference/vip-tiers/get)
* The `points.earned`, `points.redeemed`, `customer.tier_upgrade`, and `customer.tier_downgrade` webhook events
# Basics
Source: https://developers.mageloyalty.com/api-reference/metafields/basics
Learn the fundamentals of accessing Mage Loyalty metafields in Liquid.
This guide covers the basics of reading Mage Loyalty metafields in your Shopify Liquid templates.
## Accessing JSON metafields
Mage metafields are stored as JSON. In Liquid, you access the parsed data through the `.value` property:
```liquid theme={null}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
```
Once assigned to a variable, you can access nested properties using dot notation:
```liquid theme={null}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% if loyalty.points_program.enabled %}
Our loyalty program is active!
{% endif %}
```
## Checking if metafields exist
Always check that a metafield exists before using it. This prevents errors on pages where the data may not be available:
```liquid theme={null}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% if loyalty %}
{%- comment -%} Safe to use loyalty data {%- endcomment -%}
Earn points on every purchase
{% endif %}
```
For customer metafields, also ensure the customer is logged in:
```liquid theme={null}
{% if customer %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
{% if customer_loyalty %}
You have {{ customer_loyalty.points_balance }} points
{% endif %}
{% endif %}
```
## Iterating over arrays
Earning rules, rewards, and VIP tiers are arrays. Loop through them with `for`:
```liquid theme={null}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% if loyalty.points_program.enabled %}
Ways to earn
{% for rule in loyalty.points_program.ways_to_earn %}
{% if rule.is_active %}
{% for reward in loyalty.points_program.points_rewards %}
{% if reward.is_active %}
{{ reward.name }}{{ reward.points_cost }} points
{% endif %}
{% endfor %}
{% endif %}
```
## Combining shop and customer data
A common pattern is to show the customer how close they are to a reward:
```liquid theme={null}
{% if customer %}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
{% if loyalty and customer_loyalty %}
{% for reward in loyalty.points_program.points_rewards %}
{% if reward.is_active %}
{% assign points_needed = reward.points_cost | minus: customer_loyalty.points_balance %}
{% if points_needed <= 0 %}
You can redeem {{ reward.name }} now!
{% else %}
{{ points_needed }} more points until {{ reward.name }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
```
# Customer Object
Source: https://developers.mageloyalty.com/api-reference/metafields/customer-object
Reference for the customer.metafields.mage.loyalty JSON object.
The customer metafield contains the individual loyalty data for a single customer. Access it in Liquid at:
```liquid theme={null}
{{ customer.metafields.mage.loyalty.value }}
```
Since the metafield type is `json`, you access nested properties through `.value` in Liquid. This metafield is only available on pages where the `customer` object exists (i.e. the customer is logged in).
## Full object shape
```json theme={null}
{
"points_balance": 1250,
"lifetime_points": 3400,
"redeemed_points": 2150,
"vip_tier": "Gold",
"status": "member",
"points_next_expiry_at": "2026-12-31T00:00:00.000Z"
}
```
## Field reference
| Field | Type | Description |
| - | - | - |
| `points_balance` | number | Current available points the customer can spend |
| `lifetime_points` | number | Total points earned since joining the program |
| `redeemed_points` | number | Total points spent on rewards |
| `vip_tier` | string | Name of the customer's current VIP tier, or an empty string if they have no tier |
| `status` | string | `"member"` or `"excluded"` |
| `points_next_expiry_at` | string \| null | ISO 8601 date when the customer's next points expire. Populated only when the shop uses `last_purchase_date` expiry and the customer has made a purchase; otherwise `null` |
## Status values
| Value | Description |
| - | - |
| `member` | Active loyalty program member |
| `excluded` | Customer has been excluded from the loyalty program by the merchant |
# Getting Started with Mage Metafields
Source: https://developers.mageloyalty.com/api-reference/metafields/getting-started
Access your loyalty program data directly in Shopify Liquid templates using metafields.
Mage Loyalty automatically syncs loyalty data into [Shopify metafields](https://shopify.dev/docs/apps/custom-data/metafields), making it available in your Liquid templates, theme editor, and Storefront API — no custom code or API calls required.
## How it works
Mage writes two JSON metafields under the `mage.loyalty` namespace:
| Owner | Liquid access | Description |
| - | - | - |
| **Shop** | `shop.metafields.mage.loyalty` | Your full loyalty program configuration: earning rules, rewards, VIP tiers, referral settings, and any active bonus campaign |
| **Customer** | `customer.metafields.mage.loyalty` | Individual customer data — points balance, lifetime points, VIP tier, and status |
Both metafields are kept in sync automatically. Shop metafields update whenever you change your program settings in the dashboard. Customer metafields update whenever a customer earns points, redeems a reward, or changes VIP tier.
## Namespace and key
All Mage metafields use the same namespace and key:
| Property | Value |
| - | - |
| Namespace | `mage` |
| Key | `loyalty` |
| Type | `json` |
## When metafields update
| Event | What updates |
| - | - |
| Customer earns points (purchase, signup, review, etc.) | Customer metafield |
| Customer redeems a reward | Customer metafield |
| Customer VIP tier changes | Customer metafield |
| Points expire | Customer metafield |
| You edit earning rules, rewards, or tiers in the dashboard | Shop metafield |
| You enable/disable a program | Shop metafield |
# Shop Object
Source: https://developers.mageloyalty.com/api-reference/metafields/shop-object
Reference for the shop.metafields.mage.loyalty JSON object.
The shop metafield contains your full loyalty program configuration. Access it in Liquid at:
```liquid theme={null}
{{ shop.metafields.mage.loyalty.value }}
```
Since the metafield type is `json`, you access nested properties through `.value` in Liquid.
## Full object shape
```json theme={null}
{
"app_id": "f0e1d2c3b4a5968778695a4b3c2d1e0f",
"memberships_enabled": false,
"points_program": {
"enabled": true,
"ways_to_earn": [
{
"name": "Place an order",
"description": "Earn points for every dollar you spend",
"points": 5,
"type": "purchase",
"category": "orders",
"is_active": true,
"customer_facing_label": "5 points per $1 spent",
"call_to_action_url": null
}
],
"points_rewards": [
{
"name": "$5 Off",
"points_cost": 500,
"discount_type": "fixed",
"discount_amount": 5.0,
"minimum_spend": null,
"is_active": true
}
],
"points_expiry_enabled": false,
"points_expiry_days": null,
"points_expiry_mode": "date_acquired"
},
"vip_program": {
"enabled": true,
"multiplier_mode": "purchase",
"tiers": [
{
"name": "Silver",
"threshold": 0,
"multiplier": 1.0,
"badge_color": "#C0C0C0",
"badge_image_url": null,
"position": 1
},
{
"name": "Gold",
"threshold": 1000,
"multiplier": 1.5,
"badge_color": "#FFD700",
"badge_image_url": null,
"position": 2
}
]
},
"referral_program": {
"enabled": true,
"referral_rewards": [
{
"name": "10% Off for your friend",
"discount_type": "percentage",
"reward_type": "referred_customer"
},
{
"name": "200 bonus points",
"discount_type": "points",
"reward_type": "referring_customer"
}
],
"social_settings": {
"twitter": true,
"facebook": true,
"email": true,
"whatsapp": false
},
"customer_account_required": false
},
"bonus_campaign": {
"active": true,
"name": "Double Points Weekend",
"multiplier_rules": [
{
"earning_rule_name": "Place an order",
"multiplier": 2.0
}
],
"ends_at": "2026-07-01T00:00:00.000Z"
}
}
```
## Field reference
### Top level
| Field | Type | Description |
| - | - | - |
| `app_id` | string | The Mage Loyalty app identifier. Rarely needed in templates |
| `memberships_enabled` | boolean | Whether paid memberships are enabled for the shop |
| `points_program` | object | Points earning and redemption configuration |
| `vip_program` | object | VIP tier configuration |
| `referral_program` | object | Referral program configuration |
| `bonus_campaign` | object \| null | The currently active bonus campaign, or `null` when none is running |
### `points_program`
| Field | Type | Description |
| - | - | - |
| `enabled` | boolean | Whether the points program is active |
| `ways_to_earn` | array | List of earning rules |
| `points_rewards` | array | List of rewards available for redemption |
| `points_expiry_enabled` | boolean | Whether points expire |
| `points_expiry_days` | number \| null | Days until points expire (null if disabled) |
| `points_expiry_mode` | string | When the expiry clock starts: `date_acquired` or `last_purchase_date` |
### `points_program.ways_to_earn[]`
| Field | Type | Description |
| - | - | - |
| `name` | string | Earning rule name |
| `description` | string \| null | Optional description |
| `points` | number | Points awarded |
| `type` | string | Rule action type (e.g. `purchase`, `signup`, `birthday`, `follow_on_instagram`) |
| `category` | string \| null | Rule category |
| `is_active` | boolean | Whether the rule is currently active |
| `customer_facing_label` | string \| null | Display label for customers |
| `call_to_action_url` | string \| null | Optional URL for social/action rules |
### `points_program.points_rewards[]`
| Field | Type | Description |
| - | - | - |
| `name` | string | Reward name |
| `points_cost` | number | Points required to redeem |
| `discount_type` | string | Discount type (`fixed`, `percentage`, `free-shipping`, `free-product`) |
| `discount_amount` | number \| null | Discount value (null for free shipping/product) |
| `minimum_spend` | number \| null | Minimum order amount required |
| `is_active` | boolean | Whether the reward is currently active |
### `vip_program`
| Field | Type | Description |
| - | - | - |
| `enabled` | boolean | Whether the VIP program is active |
| `multiplier_mode` | string \| null | Which actions the tier multiplier applies to: `purchase` (orders only), `all` (all earning), or `null` when not configured |
| `tiers` | array | List of VIP tiers (sorted by threshold, ascending). Empty when the VIP program is disabled |
### `vip_program.tiers[]`
| Field | Type | Description |
| - | - | - |
| `name` | string | Tier name |
| `threshold` | number | Lifetime points needed to reach this tier |
| `multiplier` | number | Points earning multiplier (e.g. `1.5` = 1.5x points) |
| `badge_color` | string \| null | Hex color for the tier badge |
| `badge_image_url` | string \| null | Custom badge image URL |
| `position` | number | The tier's rank, 1-indexed by threshold ascending (e.g. Silver = 1, Gold = 2) |
### `referral_program`
| Field | Type | Description |
| - | - | - |
| `enabled` | boolean | Whether the referral program is active |
| `referral_rewards` | array | Rewards offered for referrals. Empty when the program is disabled |
| `social_settings` | object | Which sharing channels are enabled |
| `customer_account_required` | boolean | Whether referred friends must create an account |
### `referral_program.referral_rewards[]`
| Field | Type | Description |
| - | - | - |
| `name` | string | Reward name |
| `discount_type` | string | Type of discount (`percentage`, `fixed`, `points`, `free-shipping`, `free-product`) |
| `reward_type` | string | Who receives it (`referring_customer` or `referred_customer`) |
### `referral_program.social_settings`
| Field | Type | Description |
| - | - | - |
| `twitter` | boolean | Twitter/X sharing enabled |
| `facebook` | boolean | Facebook sharing enabled |
| `email` | boolean | Email sharing enabled |
| `whatsapp` | boolean | WhatsApp sharing enabled |
### `bonus_campaign`
`null` when no bonus campaign is currently active. When a campaign is running:
| Field | Type | Description |
| - | - | - |
| `active` | boolean | Always `true` while present |
| `name` | string \| null | Campaign name |
| `multiplier_rules` | array | The earning rules boosted by this campaign |
| `ends_at` | string \| null | ISO 8601 timestamp when the campaign ends |
### `bonus_campaign.multiplier_rules[]`
| Field | Type | Description |
| - | - | - |
| `earning_rule_name` | string | The name of the boosted earning rule |
| `multiplier` | number | The bonus multiplier applied during the campaign |
# VIP Tiers
Source: https://developers.mageloyalty.com/api-reference/metafields/vip-tiers
Display VIP tier information, progress bars, and tier-specific content using metafields.
Use metafields to build custom VIP tier displays — progress bars, tier badges, exclusive content, and more.
## Display all VIP tiers
List the available tiers with their requirements:
```liquid theme={null}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% if loyalty.vip_program.enabled %}
VIP Tiers
{% for tier in loyalty.vip_program.tiers %}
{{ tier.name }}
{{ tier.threshold }} lifetime points to unlock
{{ tier.multiplier }}x points on every purchase
{% endfor %}
{% endif %}
```
## Show current tier with progress to next
Combine customer and shop metafields to show tier progress:
```liquid theme={null}
{% if customer %}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
{% if loyalty.vip_program.enabled and customer_loyalty %}
{{ points_to_next }} more lifetime points to reach {{ next_tier.name }}
{% endif %}
{% else %}
You've reached the highest tier!
{% endif %}
{% endif %}
{% endif %}
```
## Tier-specific content
Show different content based on the customer's tier:
```liquid theme={null}
{% if customer %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
{% if customer_loyalty %}
{% case customer_loyalty.vip_tier %}
{% when 'Gold' %}
Welcome back, Gold member! Enjoy 1.5x points on all purchases.
{% when 'Platinum' %}
Welcome back, Platinum member! Enjoy 2x points and free shipping.
{% else %}
Earn more points to unlock VIP perks!
{% endcase %}
{% endif %}
{% endif %}
```
## Tier badge with color
Use the tier's badge color for dynamic styling:
```liquid theme={null}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
{% if loyalty.vip_program.enabled and customer_loyalty %}
{% for tier in loyalty.vip_program.tiers %}
{% if tier.name == customer_loyalty.vip_tier %}
{% if tier.badge_image_url %}
{% endif %}
{{ tier.name }}
{% break %}
{% endif %}
{% endfor %}
{% endif %}
```
## Points multiplier display
Highlight the earning multiplier for the customer's current tier:
```liquid theme={null}
{% if customer %}
{% assign loyalty = shop.metafields.mage.loyalty.value %}
{% assign customer_loyalty = customer.metafields.mage.loyalty.value %}
{% if loyalty.vip_program.enabled and customer_loyalty %}
{% for tier in loyalty.vip_program.tiers %}
{% if tier.name == customer_loyalty.vip_tier and tier.multiplier > 1 %}
You earn {{ tier.multiplier }}x points as a {{ tier.name }} member
{% break %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
```
# Pagination, Sorting & Filtering
Source: https://developers.mageloyalty.com/api-reference/pagination
How to paginate, sort, and filter results from list endpoints.
## Pagination
All list endpoints use page-number (offset) pagination, controlled by a page number and a page size parameter. There are no cursors: request a specific page directly with `page[number]`.
| Parameter | Default | Max | Description |
| - | - | - | - |
| `page[number]` | `1` | — | Page number |
| `page[size]` | `20` | `100` | Results per page |
**Pagination metadata** is included in every list response under the `meta` key. Use `total_pages` to know when to stop, and increment `page[number]` to walk through the results.
```bash cURL theme={null}
curl "https://api.mageloyalty.com/v1/customers?page[number]=2&page[size]=50" \
-H "Authorization: "
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/customers?page[number]=2&page[size]=50",
{
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/customers",
headers={"Authorization": ""},
params={"page[number]": 2, "page[size]": 50},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "customer",
"id": "6789012345",
"attributes": {
"email": "jane@example.com",
"current_points": 450
}
}
],
"meta": {
"total": 245,
"page": 2,
"per_page": 50,
"total_pages": 5
}
}
```
## Sorting
Most list endpoints accept a `sort` parameter. Prefix the field name with `-` for descending order.
```bash theme={null}
# Sort by most recent first (descending)
?sort=-created_at
# Sort by name A-Z (ascending)
?sort=name
```
Each endpoint documents its own set of sortable fields. The default sort for all list endpoints is `-created_at` (newest first).
## Filtering
There is no global filter engine. Filtering is endpoint-specific: each list endpoint defines its own `filter[field]` parameters, and only the filters documented on that endpoint are accepted. The examples below are real for the endpoints noted.
```bash theme={null}
# Rewards: filter by active status
?filter[is_active]=true
# Earning history: filter by status
?filter[status]=approved
```
See each endpoint's reference page for the filters it supports.
# Rate Limits
Source: https://developers.mageloyalty.com/api-reference/rate-limits
Rate limiting policy for the Mage Loyalty API.
## Rate limits
The Mage Loyalty API enforces a rate limit of **6 requests per second** per API key using a sliding window.
Exceeding the rate limit returns a `429 Too Many Requests` response:
```json theme={null}
{
"errors": [
{
"status": "429",
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Max 6 requests per second."
}
]
}
```
## Response headers
A `429` response includes headers that describe the limit and when you can retry:
| Header | Description |
| - | - |
| `X-RateLimit-Limit` | The maximum number of requests allowed in the window |
| `X-RateLimit-Remaining` | The number of requests remaining in the current window |
| `X-RateLimit-Reset` | The time the current window resets, as a Unix timestamp in milliseconds |
| `Retry-After` | The number of seconds to wait before retrying |
## Best practices
* **Cache responses** where possible to reduce the number of API calls
* **Use pagination** to fetch large datasets in batches rather than making many parallel requests
* **Implement exponential backoff** when you receive a `429` response
* **Use read-only keys** for high-frequency read operations like storefront widgets
# List rewards
Source: https://developers.mageloyalty.com/api-reference/rewards/list
GET /v1/rewards
List all rewards for the shop.
List all rewards for the shop. Excludes soft-deleted rewards.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `filter[is_active]` | string | Filter by active status: `true` or `false` |
| `sort` | string | Sort field. Prefix with `-` for descending. Options: `created_at`, `name`, `points_cost`. Default: `-created_at` |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Response
Returns a paginated list of [reward objects](/api-reference/rewards/object) along with a `meta` object describing the page. The `meta` also includes `loyalty_mode` and `currency` so you can read `points_cost` values correctly, and each flexible reward carries a `flexible_config` with its valid redemption range (see [Loyalty modes](/api-reference/loyalty-modes)).
```bash cURL theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/rewards?filter[is_active]=true&sort=-created_at' \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/rewards?filter[is_active]=true&sort=-created_at",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/rewards",
headers={"Authorization": ""},
params={"filter[is_active]": "true", "sort": "-created_at"},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "reward",
"id": "reward_abc",
"attributes": {
"name": "£5 off your next order",
"description": null,
"discount_type": "fixed",
"reward_type": "loyalty",
"points_cost": 500,
"discount_amount": 5.00,
"minimum_spend": 20.00,
"is_active": true,
"max_redemptions": null,
"expiry_days": 30,
"tier_name": null,
"redemption_mode": "standard",
"flexible_config": null,
"free_product": null,
"created_at": "2024-01-01T00:00:00.000Z"
}
}
],
"meta": {
"total": 3,
"page": 1,
"per_page": 20,
"total_pages": 1,
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
```json 401 Unauthorized theme={null}
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "Invalid or revoked API key"
}
]
}
```
# Reward object
Source: https://developers.mageloyalty.com/api-reference/rewards/object
The reward object represents a redeemable reward in the loyalty program.
Rewards are configured by the merchant and can be redeemed by customers using their points. Each reward generates a Shopify discount code on redemption.
## Attributes
| Field | Type | Description |
| - | - | - |
| `name` | string | Reward display name |
| `description` | string \| null | Optional description |
| `discount_type` | string | Type of discount: `fixed`, `percentage`, `free-shipping`, `free-product` |
| `reward_type` | string | `loyalty` for a standard reward, or `vip` for a VIP tier reward |
| `points_cost` | integer | Points required to redeem this reward. Minor currency units in store-credit mode (see [Loyalty modes](/api-reference/loyalty-modes)) |
| `discount_amount` | number | The discount value (e.g. `5.00` for £5 off) |
| `minimum_spend` | number \| null | Minimum order value required to use the discount |
| `is_active` | boolean | Whether the reward is currently available |
| `max_redemptions` | integer \| null | Maximum total redemptions allowed, or `null` for unlimited |
| `expiry_days` | integer \| null | Number of days the discount code is valid after redemption |
| `tier_name` | string \| null | The VIP tier this reward is exclusive to, by name. `null` when available to all members |
| `redemption_mode` | string | `standard` for a fixed redemption, or `flexible` when the customer chooses how many points to spend |
| `flexible_config` | object \| null | Redemption bounds for `flexible` rewards (see below). `null` for standard rewards |
| `free_product` | object \| null | The granted product, for `free-product` rewards (see below). `null` for all other reward types |
| `created_at` | string | ISO 8601 timestamp |
### Flexible config
For `flexible` rewards, `flexible_config` defines the range the customer can redeem. Use it to know the valid `points` range when calling [Purchase a reward](/api-reference/rewards/purchase).
| Field | Type | Description |
| - | - | - |
| `min_points` | integer | Minimum amount the customer can redeem (minor units in store-credit mode) |
| `max_points` | integer | Maximum amount the customer can redeem (minor units in store-credit mode) |
| `conversion_rate` | number | Cash discount produced per `per_points`. In store-credit mode this is pinned so 1 minor unit of credit equals 1 minor unit of discount |
| `per_points` | integer | The points unit the `conversion_rate` applies to |
### Free product details
For `free-product` rewards, `free_product` describes the product the customer receives:
| Field | Type | Description |
| - | - | - |
| `product_name` | string \| null | The product's name |
| `product_id` | string \| null | The Shopify product ID |
| `variant_id` | string \| null | The Shopify variant ID, when a specific variant is granted |
| `product_amount` | number \| null | Quantity of the product granted |
## Example
```json theme={null}
{
"type": "reward",
"id": "reward_abc",
"attributes": {
"name": "£5 off your next order",
"description": null,
"discount_type": "fixed",
"reward_type": "loyalty",
"points_cost": 500,
"discount_amount": 5.00,
"minimum_spend": 20.00,
"is_active": true,
"max_redemptions": null,
"expiry_days": 30,
"tier_name": null,
"redemption_mode": "standard",
"flexible_config": null,
"free_product": null,
"created_at": "2024-01-01T00:00:00.000Z"
}
}
```
### Flexible reward example
A flexible reward reports `redemption_mode: "flexible"` and a populated `flexible_config`:
```json theme={null}
{
"type": "reward",
"id": "reward_flex",
"attributes": {
"name": "Spend your points",
"description": null,
"discount_type": "fixed",
"reward_type": "loyalty",
"points_cost": 100,
"discount_amount": 1.00,
"minimum_spend": null,
"is_active": true,
"max_redemptions": null,
"expiry_days": 30,
"tier_name": null,
"redemption_mode": "flexible",
"flexible_config": {
"min_points": 100,
"max_points": 5000,
"conversion_rate": 0.01,
"per_points": 1
},
"free_product": null,
"created_at": "2024-01-01T00:00:00.000Z"
}
}
```
# Purchase a reward
Source: https://developers.mageloyalty.com/api-reference/rewards/purchase
POST /v1/rewards/{id}/purchase
Redeem a reward for a customer, deducting points and generating a discount code.
Purchase a reward on behalf of a customer, deducting the required points from their balance and generating a Shopify discount code.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `customer_identifier` | string | Yes | Shopify customer ID or email address |
| `points` | integer | For flexible rewards | How many points to spend. Used only by flexible rewards, where the customer chooses the amount. Must fall within the reward's `flexible_config` range. On store-credit shops this is a minor-unit amount. Also accepted as `points_to_redeem` |
```json theme={null}
{
"customer_identifier": "6789012345"
}
```
Most rewards have a fixed `points_cost`, so the request only needs `customer_identifier`. For flexible rewards (`redemption_mode: "flexible"`), the customer chooses how many points to spend within the reward's `flexible_config` range (see [List rewards](/api-reference/rewards/list)), and the discount is sized to match. Pass `points` for these:
```json theme={null}
{
"customer_identifier": "6789012345",
"points": 250
}
```
On store-credit shops, only **flexible** rewards can be purchased. A standard fixed-cost reward returns `403 Forbidden` with `This reward cannot be redeemed in store credit mode`. See [Loyalty modes](/api-reference/loyalty-modes).
## Validations
* Customer must exist and not be excluded from the loyalty program
* Reward must be active and not deleted
* If the reward is restricted to a VIP tier, the customer must be on that tier
* Customer must have enough points to cover the reward's `points_cost`
* For flexible rewards, `points` is required and must be between the reward's configured minimum and maximum
## Response
Returns `201 Created`. The `discount_code` is the generated Shopify discount code for the customer to use, and `expires_at` is when that code stops working (`null` when the reward has no expiry configured). For flexible rewards, `reward_name` reflects the resolved discount (for example `£2.50 off your order`) and `points_spent` is the number of points the customer chose.
The response carries `loyalty_mode` and `currency`. `points_spent` is a minor-unit amount on store-credit shops, where the response also includes a `credit_spent` companion (a major-unit decimal string) that is absent in points mode. See [Loyalty modes](/api-reference/loyalty-modes).
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/rewards/reward_abc/purchase \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"customer_identifier": "6789012345"
}'
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/rewards/reward_abc/purchase",
{
method: "POST",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_identifier: "6789012345",
}),
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/rewards/reward_abc/purchase",
headers={"Authorization": ""},
json={"customer_identifier": "6789012345"},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"customer_identifier" => "6789012345",
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 201 Created theme={null}
{
"data": {
"type": "reward_history_entry",
"id": "cr_abc123",
"attributes": {
"reward_id": "reward_abc",
"reward_name": "£5 off your next order",
"reward_discount_type": "fixed",
"points_spent": 500,
"loyalty_mode": "points",
"currency": "GBP",
"discount_code": "MAGE-XYZ789",
"status": "active",
"expires_at": "2024-07-01T00:00:00.000Z"
}
}
}
```
```json 201 Created (store-credit shop) theme={null}
{
"data": {
"type": "reward_history_entry",
"id": "cr_abc123",
"attributes": {
"reward_id": "reward_flex",
"reward_name": "£2.50 off your order",
"reward_discount_type": "fixed",
"points_spent": 250,
"loyalty_mode": "store_credit",
"currency": "GBP",
"credit_spent": "2.50",
"discount_code": "MAGE-XYZ789",
"status": "active",
"expires_at": "2024-07-01T00:00:00.000Z"
}
}
}
```
```json 403 Store credit mode theme={null}
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "This reward cannot be redeemed in store credit mode"
}
]
}
```
```json 400 Bad Request theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "Entry rewards cannot be purchased"
}
]
}
```
```json 403 Not Eligible theme={null}
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "Customer is not eligible for this reward"
}
]
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Customer '6789012345' not found"
}
]
}
```
```json 422 Insufficient Points theme={null}
{
"errors": [
{
"status": "422",
"title": "Insufficient Points",
"detail": "Customer has 200 points but this reward costs 500"
}
]
}
```
```json 422 Invalid Points (flexible) theme={null}
{
"errors": [
{
"status": "422",
"title": "Invalid Points",
"detail": "Minimum redemption is 100 points"
}
]
}
```
```json 500 Internal Server Error theme={null}
{
"errors": [
{
"status": "500",
"title": "Internal Server Error",
"detail": "Failed to create discount code. Points have not been deducted."
}
]
}
```
## Errors
| Status | Scenario |
| - | - |
| `400` | `customer_identifier` missing, invalid JSON, the reward is an entry reward (`Entry rewards cannot be purchased`), or `points` is missing for a flexible reward (`points is required for flexible rewards`) |
| `403` | Customer is excluded from the loyalty program, not on the required VIP tier (`Customer is not eligible for this reward`), or the shop runs on store credit and the reward is not flexible (`This reward cannot be redeemed in store credit mode`) |
| `404` | Customer or reward not found |
| `422` | Customer does not have enough points (`Customer has X points but this reward costs Y`), or `points` is outside a flexible reward's range (`Minimum redemption is X points` / `Maximum redemption is X points`). On store-credit shops these amounts are formatted as money, for example `Minimum redemption is £1.00` |
| `500` | The discount code could not be generated. The points are refunded (`Failed to create discount code. Points have not been deducted.`) |
| `503` | Unable to connect to Shopify to generate the discount code |
# Retrieve a VIP tier
Source: https://developers.mageloyalty.com/api-reference/vip-tiers/get
GET /v1/vip-tiers/{id}
Fetch a single VIP tier by ID.
Fetch a single VIP tier by its ID.
## Response
Returns the [VIP tier object](/api-reference/vip-tiers/object) including its `threshold`, its `position` (1-indexed by `threshold` ascending), and any tier-exclusive rewards. The response `meta` includes `loyalty_mode` and `currency`; in store-credit mode the `threshold` and `points_cost` values are minor currency units (see [Loyalty modes](/api-reference/loyalty-modes)).
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/vip-tiers/tier_gold \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/vip-tiers/tier_gold",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/vip-tiers/tier_gold",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "vip_tier",
"id": "tier_gold",
"attributes": {
"name": "Gold",
"description": "Premium tier",
"threshold": 500,
"threshold_points": null,
"threshold_spend": null,
"threshold_orders": null,
"points_multiplier": 1.5,
"position": 2,
"badge_image_url": null,
"badge_color": "#FFD700",
"rewards": [
{
"id": "reward_abc",
"name": "£5 off your next order",
"description": null,
"discount_type": "fixed",
"reward_type": "vip",
"points_cost": 500,
"discount_amount": 5.00,
"minimum_spend": 20.00,
"is_active": true,
"max_redemptions": null,
"expiry_days": 30,
"created_at": "2024-01-01T00:00:00.000Z"
}
],
"created_at": "2024-01-01T00:00:00.000Z"
}
},
"meta": {
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "VIP tier 'tier_gold' not found"
}
]
}
```
## Errors
| Status | Scenario |
| - | - |
| `404` | VIP tier not found |
# List VIP tiers
Source: https://developers.mageloyalty.com/api-reference/vip-tiers/list
GET /v1/vip-tiers
List all VIP tiers for the shop.
List all VIP tiers for the shop, ordered by `threshold` ascending.
No pagination parameters — all tiers are returned in a single response. Most shops have 3-5 tiers.
## Response
Returns an array of [VIP tier objects](/api-reference/vip-tiers/object). The `meta` object reflects the full set returned in one page, and includes `loyalty_mode` and `currency`. In store-credit mode the tier `threshold` values are minor currency units (see [Loyalty modes](/api-reference/loyalty-modes)).
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/vip-tiers \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch("https://api.mageloyalty.com/v1/vip-tiers", {
method: "GET",
headers: {
Authorization: "",
},
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/vip-tiers",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "vip_tier",
"id": "tier_bronze",
"attributes": {
"name": "Bronze",
"description": "Entry level tier",
"threshold": 0,
"threshold_points": null,
"threshold_spend": null,
"threshold_orders": null,
"points_multiplier": 1.0,
"position": 1,
"badge_image_url": null,
"badge_color": "#CD7F32",
"rewards": [],
"created_at": "2024-01-01T00:00:00.000Z"
}
},
{
"type": "vip_tier",
"id": "tier_gold",
"attributes": {
"name": "Gold",
"description": "Premium tier",
"threshold": 500,
"threshold_points": null,
"threshold_spend": null,
"threshold_orders": null,
"points_multiplier": 1.5,
"position": 2,
"badge_image_url": null,
"badge_color": "#FFD700",
"rewards": [
{
"id": "reward_abc",
"name": "£5 off your next order",
"description": null,
"discount_type": "fixed",
"reward_type": "vip",
"points_cost": 500,
"discount_amount": 5.00,
"minimum_spend": 20.00,
"is_active": true,
"max_redemptions": null,
"expiry_days": 30,
"created_at": "2024-01-01T00:00:00.000Z"
}
],
"created_at": "2024-01-01T00:00:00.000Z"
}
}
],
"meta": {
"total": 2,
"page": 1,
"per_page": 2,
"total_pages": 1,
"entry_method": "points",
"entry_methods_extra": [],
"entry_logic": "AND",
"points_multiplier_mode": "purchase",
"loyalty_mode": "points",
"currency": "GBP"
}
}
```
## Response meta
Alongside the standard pagination fields, the `meta` object describes how customers qualify for tiers across the shop. Use it to interpret each tier's `threshold` and per-criterion thresholds.
| Field | Type | Description |
| - | - | - |
| `entry_method` | string | The shop's primary entry method: `points`, `spend`, or `orders`. This is what each tier's `threshold` is measured in |
| `entry_methods_extra` | array | Any additional entry methods in use (a subset of `points`, `spend`, `orders`). Empty when only the primary method applies |
| `entry_logic` | string | How the primary and additional methods combine when more than one is in use: `AND` (meet all) or `OR` (meet any) |
| `points_multiplier_mode` | string \| null | Which earning the tier multiplier applies to: `purchase`, `all`, or `null` when not configured |
# VIP tier object
Source: https://developers.mageloyalty.com/api-reference/vip-tiers/object
The VIP tier object represents a loyalty tier level.
VIP tiers define the levels of your loyalty program. Customers are automatically assigned to tiers based on their lifetime points.
## Attributes
| Field | Type | Description |
| - | - | - |
| `name` | string | Tier display name |
| `description` | string \| null | Optional description |
| `threshold` | integer | Entry value for the shop's primary entry method (see `entry_method` on the [list endpoint](/api-reference/vip-tiers/list)). For most shops this is lifetime points. Minor currency units when the entry method is points on a store-credit shop (see [Loyalty modes](/api-reference/loyalty-modes)) |
| `threshold_points` | integer \| null | Points required when points is an additional entry method, otherwise `null` |
| `threshold_spend` | integer \| null | Spend required when spend is an additional entry method, otherwise `null` |
| `threshold_orders` | integer \| null | Orders required when orders is an additional entry method, otherwise `null` |
| `points_multiplier` | number | Points earning multiplier for members of this tier |
| `position` | integer | The tier's rank, 1-indexed by `threshold` ascending (the lowest tier is `1`) |
| `badge_image_url` | string \| null | URL to the tier badge image |
| `badge_color` | string \| null | Hex color code for the tier badge |
| `rewards` | array | Tier-exclusive rewards automatically granted to members |
| `created_at` | string | ISO 8601 timestamp |
### Embedded rewards
Each item in the `rewards` array contains:
| Field | Type | Description |
| - | - | - |
| `id` | string | Reward ID |
| `name` | string | Reward name |
| `description` | string \| null | Optional description |
| `discount_type` | string | Discount type |
| `reward_type` | string | The reward's type, typically `vip` for tier-exclusive rewards |
| `points_cost` | integer | Points cost. Minor currency units in store-credit mode |
| `discount_amount` | number | Discount value |
| `minimum_spend` | number \| null | Minimum order value |
| `is_active` | boolean | Whether the reward is active |
| `max_redemptions` | integer \| null | Maximum redemptions |
| `expiry_days` | integer \| null | Days until discount code expires |
| `created_at` | string | ISO 8601 timestamp |
## Example
```json theme={null}
{
"type": "vip_tier",
"id": "tier_gold",
"attributes": {
"name": "Gold",
"description": "Premium tier",
"threshold": 500,
"threshold_points": null,
"threshold_spend": null,
"threshold_orders": null,
"points_multiplier": 1.5,
"position": 2,
"badge_image_url": null,
"badge_color": "#FFD700",
"rewards": [
{
"id": "reward_abc",
"name": "£5 off your next order",
"description": null,
"discount_type": "fixed",
"reward_type": "vip",
"points_cost": 500,
"discount_amount": 5.00,
"minimum_spend": 20.00,
"is_active": true,
"max_redemptions": null,
"expiry_days": 30,
"created_at": "2024-01-01T00:00:00.000Z"
}
],
"created_at": "2024-01-01T00:00:00.000Z"
}
}
```
# custom_reward.redeemed
Source: https://developers.mageloyalty.com/api-reference/webhooks/custom-reward-redeemed
Fired when a customer redeems a custom reward, so your system can fulfil it.
Fired when a customer redeems a [custom reward](https://help.mageloyalty.com) — a reward you fulfil yourself, such as a consultation, an event invite, or a gift sent by hand. Custom rewards do not generate a discount code, so this webhook is how your systems find out that a customer is waiting on you.
At this point the points have been deducted and the redemption is recorded. It does **not** mean the customer has received the reward yet — that is tracked separately when you mark the redemption as used in the Mage Loyalty dashboard.
## Configuring this event
This event cannot be subscribed to from **Settings > Webhooks** or through the [subscriptions API](/api-reference/webhooks/subscriptions/list). It is managed by the reward itself.
To receive it, edit your custom reward in the Mage Loyalty dashboard, set **Notification method** to `Webhook` (or `Email & Webhook`) and enter your endpoint URL. Mage Loyalty creates a webhook subscription for that reward automatically and displays its signing secret once.
You can also set a **Reward identifier** — a short handle such as `surprise-gift-next-order` that is sent as `reward_identifier` so your automation can tell rewards apart without relying on the display name. Leave it blank and Mage Loyalty uses a slug of the reward name.
The subscription then behaves like any other: it appears in **Settings > Webhooks** marked *Managed by reward*, where you can send a test request and inspect delivery logs. Changing or removing the URL is done on the reward, and deleting the reward deletes the subscription.
The reward form also has a **Send test webhook** button, which delivers a payload with `test` set to `true` so you can confirm your endpoint before any customer redeems.
## Payload fields
| Field | Type | Description |
| - | - | - |
| `test` | boolean | `true` when sent by the **Send test webhook** button, `false` for a real redemption |
| `reward_identifier` | string | The reward identifier you set on the custom reward. If you left it blank, a slug of the reward name (e.g. `virtual-styling-session`) |
| `reward_name` | string | Name of the custom reward that was redeemed |
| `shopify_customer_id` | string \| null | The customer's Shopify customer ID |
| `customer_email` | string \| null | The customer's email address |
| `customer_first_name` | string \| null | The customer's first name |
| `customer_last_name` | string \| null | The customer's last name |
| `points_spent` | integer | Number of points spent on this redemption |
| `redeemed_at` | string | ISO 8601 timestamp of when the customer redeemed the reward |
## Store-credit shops
Custom rewards are available on points-mode shops only, so this event never carries the `loyalty_mode`, `currency` or `credit_*` companion fields described in [Loyalty modes](/api-reference/loyalty-modes). `points_spent` is always a points value.
## Delivery and retries
Responding with any `2xx` status code marks the redemption's notification as delivered. Anything else is retried up to **5 times** with exponential backoff — more than the 3 retries used by other events, because this webhook is often the only signal that a customer is waiting.
After the final failed attempt the redemption is flagged as failed in the dashboard. The redemption itself is never reversed: the customer keeps the reward and it still appears on the reward's **Redemptions** page, which lists every customer who has redeemed regardless of webhook outcome.
## Example payload
```json custom_reward.redeemed theme={null}
{
"event": "custom_reward.redeemed",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"test": false,
"reward_identifier": "virtual-styling-session",
"reward_name": "Virtual styling session",
"shopify_customer_id": "6789012345",
"customer_email": "jane@example.com",
"customer_first_name": "Jane",
"customer_last_name": "Doe",
"points_spent": 5000,
"redeemed_at": "2026-02-18T11:59:58.000Z"
}
}
```
```json custom_reward.redeemed (test delivery) theme={null}
{
"event": "custom_reward.redeemed",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"test": true,
"reward_identifier": "virtual-styling-session",
"reward_name": "Virtual styling session",
"shopify_customer_id": "0000000000",
"customer_email": "owner@my-store.com",
"customer_first_name": "Test",
"customer_last_name": "Customer",
"points_spent": 5000,
"redeemed_at": "2026-02-18T12:00:00.000Z"
}
}
```
## Common use cases
* Trigger a Zapier or Make automation that adds the customer to a booking sheet or CRM
* Create a ticket in a helpdesk so your team can arrange the reward
* Notify a fulfilment partner that a hand-packed gift needs sending
* Post to a Slack channel so whoever is on duty can follow up
# customer.tier_downgrade
Source: https://developers.mageloyalty.com/api-reference/webhooks/customer-tier-downgrade
Fired when a customer is moved down to a lower VIP tier.
Fired when a customer's lifetime points no longer meet the threshold for their current VIP tier and they are moved to a lower one. This typically happens on a lifetime points recalcuation.
## Payload fields
| Field | Type | Description |
| - | - | - |
| `shopify_customer_id` | string | The customer's Shopify customer ID |
| `email` | string \| null | The customer's email address |
| `previous_tier` | string | The tier the customer was on before the downgrade. Always a string: a tier name, or the literal string `"None"` if the customer had no prior tier (never `null`) |
| `new_tier` | string | The tier the customer has been moved to |
| `lifetime_points` | integer | The customer's all-time total earned points |
## Store-credit shops
On store-credit shops, `lifetime_points` is a minor currency unit amount, and `data` carries `loyalty_mode` (`store_credit`), `currency`, and a major-unit decimal-string companion `lifetime_credit`. Points-mode payloads do not include these fields. See [Loyalty modes](/api-reference/loyalty-modes).
## Example payload
```json customer.tier_downgrade theme={null}
{
"event": "customer.tier_downgrade",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"previous_tier": "Gold",
"new_tier": "Silver",
"lifetime_points": 800
}
}
```
```json customer.tier_downgrade (store-credit shop) theme={null}
{
"event": "customer.tier_downgrade",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"previous_tier": "Gold",
"new_tier": "Silver",
"lifetime_points": 800,
"loyalty_mode": "store_credit",
"currency": "GBP",
"lifetime_credit": "8.00"
}
}
```
## Common use cases
* Send a re-engagement email encouraging the customer to earn more points
* Update the customer's segment or tag in your marketing platform
* Offer a limited-time bonus points promotion to help them regain their tier
* Log tier changes for churn analysis and reporting
# customer.tier_upgrade
Source: https://developers.mageloyalty.com/api-reference/webhooks/customer-tier-upgrade
Fired when a customer is promoted to a higher VIP tier.
Fired when a customer accumulates enough lifetime points to move up to a higher VIP tier. The event includes both the previous and new tier names so you can tailor your response.
## Payload fields
| Field | Type | Description |
| - | - | - |
| `shopify_customer_id` | string | The customer's Shopify customer ID |
| `email` | string \| null | The customer's email address |
| `previous_tier` | string | The tier the customer was on before the upgrade. Always a string: a tier name, or the literal string `"None"` if the customer had no prior tier (never `null`) |
| `new_tier` | string | The tier the customer has been promoted to |
| `lifetime_points` | integer | The customer's all-time total earned points |
## Store-credit shops
On store-credit shops, `lifetime_points` is a minor currency unit amount, and `data` carries `loyalty_mode` (`store_credit`), `currency`, and a major-unit decimal-string companion `lifetime_credit`. Points-mode payloads do not include these fields. See [Loyalty modes](/api-reference/loyalty-modes).
## Example payload
```json customer.tier_upgrade theme={null}
{
"event": "customer.tier_upgrade",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"previous_tier": "Silver",
"new_tier": "Gold",
"lifetime_points": 1200
}
}
```
```json customer.tier_upgrade (store-credit shop) theme={null}
{
"event": "customer.tier_upgrade",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"previous_tier": "Silver",
"new_tier": "Gold",
"lifetime_points": 1200,
"loyalty_mode": "store_credit",
"currency": "GBP",
"lifetime_credit": "12.00"
}
}
```
```json First-ever tier (no prior tier) theme={null}
{
"event": "customer.tier_upgrade",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"previous_tier": "None",
"new_tier": "Silver",
"lifetime_points": 500
}
}
```
## Common use cases
* Send a congratulatory email or SMS welcoming the customer to their new tier
* Update the customer's segment or tag in your marketing platform
* Trigger a special one-time reward or gift for reaching the new tier
* Log tier progression data for analytics and reporting
# Getting Started with Webhooks
Source: https://developers.mageloyalty.com/api-reference/webhooks/getting-started
Receive real-time HTTP notifications when loyalty events happen in your store.
Webhooks let your server receive real-time `POST` requests whenever something happens in your loyalty program — a customer earns points, redeems a reward, or moves between VIP tiers.
Instead of polling the API, you register an endpoint URL and choose which events you care about. Mage Loyalty sends a signed JSON payload to your URL within seconds of the event occurring.
## Requirements
* A **Growth** plan or above
* A publicly accessible HTTPS endpoint that can receive `POST` requests
## How it works
In the Mage Loyalty dashboard, go to **Settings > Webhooks** and create a new subscription. Provide your endpoint URL and select the events you want to receive.
When you create a subscription, a **signing secret** is displayed once. Copy and store it securely — you'll need it to verify that incoming requests are genuinely from Mage Loyalty.
When an event fires, Mage Loyalty sends a `POST` request to your endpoint. Your server should [verify the signature](/api-reference/webhooks/verifying-signatures) and then process the payload.
Respond with any `2xx` status code to acknowledge receipt. Non-2xx responses are treated as failures and the delivery will be retried up to 3 times.
## Payload format
Every webhook request has the same top-level structure:
```json theme={null}
{
"event": "points.earned",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
// Event-specific fields — see individual event pages
}
}
```
| Field | Type | Description |
| - | - | - |
| `event` | string | The event type (e.g. `points.earned`) |
| `shop` | string | Your Shopify store identifier |
| `timestamp` | string | ISO 8601 timestamp of when the event occurred |
| `data` | object | Event-specific payload, varies by event type |
On store-credit shops, points events carry the loyalty value in minor currency units and add `loyalty_mode`, `currency`, and major-unit `credit_*` companion fields to `data`. Points-mode payloads are unchanged. See [Loyalty modes](/api-reference/loyalty-modes).
## Request headers
Every webhook request includes these headers:
| Header | Example | Description |
| - | - | - |
| `X-Webhook-Signature` | `sha256=a1b2c3...` | HMAC-SHA256 signature for verification |
| `X-Webhook-Timestamp` | `2026-02-18T12:00:00.000Z` | Timestamp used in the signing string |
| `X-Webhook-Event` | `points.earned` | The event type |
| `Content-Type` | `application/json` | Always JSON |
| `User-Agent` | `MageLoyalty-Webhook/1.0` | Identifies the sender |
## Retries
If your endpoint returns a non-2xx status code or the connection fails, the delivery is retried up to **3 times** with exponential backoff.
## Testing
You can send a test request from the dashboard by clicking **Test** next to any subscription. This is a connectivity check, not one of the subscribable loyalty events: it delivers a payload with `event` set to `"test"` (matching the `X-Webhook-Event: test` header) and a `data` object containing a single `message` field. It is signed with your subscription's signing secret exactly like a real delivery, so you can use it to confirm your endpoint and signature verification are working before any live events fire.
## Managing subscriptions programmatically
You can also create and manage webhook subscriptions through the API instead of the dashboard. See:
* [List webhook subscriptions](/api-reference/webhooks/subscriptions/list)
* [Create a webhook subscription](/api-reference/webhooks/subscriptions/create)
* [Update a webhook subscription](/api-reference/webhooks/subscriptions/update)
* [Delete a webhook subscription](/api-reference/webhooks/subscriptions/delete)
* [Rotate a signing secret](/api-reference/webhooks/subscriptions/rotate-secret)
## Available events
| Event | Description |
| - | - |
| [`points.earned`](/api-reference/webhooks/points-earned) | A customer earned points |
| [`points.redeemed`](/api-reference/webhooks/points-redeemed) | A customer redeemed points for a reward |
| [`customer.tier_upgrade`](/api-reference/webhooks/customer-tier-upgrade) | A customer moved up to a higher VIP tier |
| [`customer.tier_downgrade`](/api-reference/webhooks/customer-tier-downgrade) | A customer moved down to a lower VIP tier |
| [`custom_reward.redeemed`](/api-reference/webhooks/custom-reward-redeemed) | A customer redeemed a custom reward you fulfil yourself |
`custom_reward.redeemed` is not subscribable here or through the API. It is configured on the custom reward itself — see [`custom_reward.redeemed`](/api-reference/webhooks/custom-reward-redeemed).
# points.earned
Source: https://developers.mageloyalty.com/api-reference/webhooks/points-earned
Fired when a customer earns points through any channel — purchases, earning rules, manual adjustments, or referrals.
Fired when a customer earns points. This covers all sources: order-based earning rules, custom earning rules, manual adjustments, and referral bonuses.
## Payload fields
| Field | Type | Description |
| - | - | - |
| `shopify_customer_id` | string | The customer's Shopify customer ID |
| `email` | string \| null | The customer's email address |
| `points_earned` | integer | Number of points earned in this event |
| `points_balance` | integer | The customer's total available points after this event |
| `lifetime_points` | integer | The customer's all-time total earned points |
| `source` | string | How the points were earned. One of `order`, `earning_rule`, `manual`, `referral`, or `approval_cron` (points released after an approval delay) |
| `earning_rule_name` | string \| null | Name of the earning rule, if applicable |
| `order_number` | string \| null | Shopify order number, if the points were earned from a purchase |
## Store-credit shops
On store-credit shops, the point values above are minor currency units, and `data` carries three extra fields: `loyalty_mode` (`store_credit`), `currency`, and major-unit decimal-string companions `credit_earned` and `credit_balance`. Points-mode payloads do not include these fields. See [Loyalty modes](/api-reference/loyalty-modes).
## Example payload
```json points.earned theme={null}
{
"event": "points.earned",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"points_earned": 150,
"points_balance": 700,
"lifetime_points": 1200,
"source": "earning_rule",
"earning_rule_name": "Purchase reward",
"order_number": "1042"
}
}
```
```json points.earned (store-credit shop) theme={null}
{
"event": "points.earned",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"points_earned": 150,
"points_balance": 700,
"lifetime_points": 1200,
"source": "earning_rule",
"earning_rule_name": "Purchase reward",
"order_number": "1042",
"loyalty_mode": "store_credit",
"currency": "GBP",
"credit_earned": "1.50",
"credit_balance": "7.00"
}
}
```
## Common use cases
* Sync points to an external CRM or data warehouse
* Trigger a personalised email or SMS when a customer hits a milestone (e.g. 1000 lifetime points)
* Display a real-time notification on your storefront
* Update a customer profile in your CRM or marketing platform
# points.redeemed
Source: https://developers.mageloyalty.com/api-reference/webhooks/points-redeemed
Fired when a customer redeems points for a reward, generating a discount code.
Fired when a customer successfully redeems points for a reward. At this point the points have been deducted and a discount code has been generated.
## Payload fields
| Field | Type | Description |
| - | - | - |
| `shopify_customer_id` | string | The customer's Shopify customer ID |
| `email` | string \| null | The customer's email address |
| `points_redeemed` | integer | Number of points spent on this redemption |
| `points_balance` | integer | The customer's remaining available points after this redemption |
| `reward_name` | string | Name of the reward that was redeemed |
| `reward_id` | string \| null | The reward's Mage Loyalty ID |
| `discount_code` | string \| null | The Shopify discount code generated for this redemption |
## Store-credit shops
On store-credit shops, `points_redeemed` and `points_balance` are minor currency units, and `data` carries `loyalty_mode` (`store_credit`), `currency`, and major-unit decimal-string companions `credit_redeemed` and `credit_balance`. Points-mode payloads do not include these fields. See [Loyalty modes](/api-reference/loyalty-modes).
## Example payload
```json points.redeemed theme={null}
{
"event": "points.redeemed",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"points_redeemed": 500,
"points_balance": 200,
"reward_name": "£10 off",
"reward_id": "clrew456def",
"discount_code": "MAGE-A1B2C3"
}
}
```
```json points.redeemed (store-credit shop) theme={null}
{
"event": "points.redeemed",
"shop": "my-store",
"timestamp": "2026-02-18T12:00:00.000Z",
"data": {
"shopify_customer_id": "6789012345",
"email": "jane@example.com",
"points_redeemed": 500,
"points_balance": 200,
"reward_name": "£5.00 off your order",
"reward_id": "clrew456def",
"discount_code": "MAGE-A1B2C3",
"loyalty_mode": "store_credit",
"currency": "GBP",
"credit_redeemed": "5.00",
"credit_balance": "2.00"
}
}
```
## Common use cases
* Send a follow-up email reminding the customer to use their discount code
* Log redemptions in an external analytics or BI tool
* Trigger a congratulatory push notification
* Sync discount code data to a fulfilment or ERP system
# Create a webhook subscription
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/create
POST /v1/webhook-subscriptions
Register a URL to receive loyalty event notifications and get a signing secret.
Register an HTTPS endpoint to receive webhook deliveries for the events you select. Mage Loyalty signs every delivery with the signing secret returned here, so you can verify that requests are genuine.
The `secret` is returned **only** in this `201 Created` response. It is shown once and never again. Store it securely. If you lose it, [rotate the secret](/api-reference/webhooks/subscriptions/rotate-secret) to issue a new one. See [Verifying webhook signatures](/api-reference/webhooks/verifying-signatures) for how to use it.
## Request body
| Field | Type | Required | Description |
| - | - | - | - |
| `url` | string | Yes | The HTTPS endpoint that will receive deliveries. Must be a valid URL |
| `events` | string\[] | Yes | A non-empty array of events to subscribe to. Valid events: `points.earned`, `points.redeemed`, `customer.tier_upgrade`, `customer.tier_downgrade` |
| `description` | string | No | Optional label to help you identify the subscription |
## Response
Returns `201 Created` with the new subscription, including the `secret`.
## Errors
| Status | Scenario |
| - | - |
| `400` | `url` missing, `url` not a valid URL, `events` empty or missing, one or more events invalid, or invalid JSON |
| `401` | Missing or invalid API key |
| `403` | The API key is read-only and cannot perform write operations |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/webhook-subscriptions \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"description": "Production points + tier sync"
}'
```
```javascript Node theme={null}
const response = await fetch("https://api.mageloyalty.com/v1/webhook-subscriptions", {
method: "POST",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/webhooks/mage",
events: ["points.earned", "customer.tier_upgrade"],
description: "Production points + tier sync",
}),
});
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/webhook-subscriptions",
headers={"Authorization": ""},
json={
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"description": "Production points + tier sync",
},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"url" => "https://example.com/webhooks/mage",
"events" => ["points.earned", "customer.tier_upgrade"],
"description" => "Production points + tier sync",
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 201 Created theme={null}
{
"data": {
"type": "webhook_subscription",
"id": "whsub_abc123",
"attributes": {
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"is_active": true,
"description": "Production points + tier sync",
"secret": "9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
}
}
```
```json 400 Bad Request theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "url is required"
}
]
}
```
```json 400 Invalid URL theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "url must be a valid URL"
}
]
}
```
```json 400 Empty events theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "events must be a non-empty array"
}
]
}
```
```json 400 Invalid events theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "Invalid event(s): points.created. Valid events: points.earned, points.redeemed, customer.tier_upgrade, customer.tier_downgrade"
}
]
}
```
# Delete a webhook subscription
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/delete
DELETE /v1/webhook-subscriptions/{id}
Permanently delete a webhook subscription and stop all deliveries to it.
Permanently delete a webhook subscription. Deliveries to its URL stop immediately. This cannot be undone. To pause deliveries without deleting, set `is_active` to `false` with [update](/api-reference/webhooks/subscriptions/update) instead.
## Path parameters
| Parameter | Type | Description |
| - | - | - |
| `id` | string | The webhook subscription ID |
## Response
Returns `204 No Content` with an **empty response body** on success.
## Errors
| Status | Scenario |
| - | - |
| `401` | Missing or invalid API key |
| `403` | The API key is read-only and cannot perform write operations |
| `404` | No subscription with this ID exists for the shop |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request DELETE \
--url https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123 \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123",
{
method: "DELETE",
headers: {
Authorization: "",
},
}
);
// 204 No Content — response body is empty
const ok = response.status === 204;
```
```python Python theme={null}
import requests
response = requests.delete(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123",
headers={"Authorization": ""},
)
# 204 No Content — response body is empty
ok = response.status_code == 204
```
```php PHP theme={null}
"DELETE",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
curl_exec($ch);
// 204 No Content — response body is empty
$ok = curl_getinfo($ch, CURLINFO_HTTP_CODE) === 204;
```
```text 204 No Content theme={null}
(empty response body)
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Webhook subscription 'whsub_abc123' not found"
}
]
}
```
# List delivery logs
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/delivery-logs
GET /v1/webhook-subscriptions/{id}/delivery-logs
List the delivery attempts for a webhook subscription to debug failures.
List the delivery attempts for a webhook subscription, newest first. Use this to confirm that events reached your endpoint, and to debug failures by inspecting the response status code and any error message.
## Path parameters
| Parameter | Type | Description |
| - | - | - |
| `id` | string | The webhook subscription ID |
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `filter[event]` | string | Only return deliveries for this event (e.g. `points.earned`) |
| `filter[success]` | string | Filter by outcome: `true` for successful deliveries, `false` for failures |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Delivery log fields
| Field | Type | Description |
| - | - | - |
| `event` | string | The event that was delivered |
| `status_code` | integer \| null | The HTTP status code your endpoint returned, or `null` if no response was received |
| `success` | boolean | Whether the delivery was considered successful |
| `error` | string \| null | An error message when the delivery failed, otherwise `null` |
| `delivered_at` | string \| null | ISO 8601 timestamp of delivery, or `null` if it was never delivered |
| `created_at` | string | ISO 8601 timestamp of when the attempt was recorded |
## Errors
| Status | Scenario |
| - | - |
| `401` | Missing or invalid API key |
| `404` | No subscription with this ID exists for the shop |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123/delivery-logs?filter[success]=false&page[number]=1&page[size]=20' \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123/delivery-logs?filter[success]=false&page[number]=1&page[size]=20",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123/delivery-logs",
headers={"Authorization": ""},
params={"filter[success]": "false", "page[number]": 1, "page[size]": 20},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "webhook_delivery_log",
"id": "whlog_def456",
"attributes": {
"event": "points.earned",
"status_code": 500,
"success": false,
"error": "Endpoint returned 500 Internal Server Error",
"delivered_at": "2024-01-01T00:00:01.000Z",
"created_at": "2024-01-01T00:00:01.000Z"
}
},
{
"type": "webhook_delivery_log",
"id": "whlog_abc123",
"attributes": {
"event": "points.earned",
"status_code": 200,
"success": true,
"error": null,
"delivered_at": "2024-01-01T00:00:00.000Z",
"created_at": "2024-01-01T00:00:00.000Z"
}
}
],
"meta": {
"total": 2,
"page": 1,
"per_page": 20,
"total_pages": 1
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Webhook subscription 'whsub_abc123' not found"
}
]
}
```
# Retrieve a webhook subscription
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/get
GET /v1/webhook-subscriptions/{id}
Retrieve a single webhook subscription by its ID.
Retrieve a single webhook subscription by its ID. The signing secret is never included in this response.
## Path parameters
| Parameter | Type | Description |
| - | - | - |
| `id` | string | The webhook subscription ID |
## Errors
| Status | Scenario |
| - | - |
| `401` | Missing or invalid API key |
| `404` | No subscription with this ID exists for the shop |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123 \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "webhook_subscription",
"id": "whsub_abc123",
"attributes": {
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"is_active": true,
"description": "Production points + tier sync",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Webhook subscription 'whsub_abc123' not found"
}
]
}
```
# List webhook subscriptions
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/list
GET /v1/webhook-subscriptions
List all webhook subscriptions for the shop.
List all webhook subscriptions for the shop, newest first. The signing secret is never included in this response.
## Query parameters
| Parameter | Type | Description |
| - | - | - |
| `page[number]` | integer | Page number (default: `1`) |
| `page[size]` | integer | Results per page (default: `20`, max: `100`) |
## Errors
| Status | Scenario |
| - | - |
| `401` | Missing or invalid API key |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request GET \
--url 'https://api.mageloyalty.com/v1/webhook-subscriptions?page[number]=1&page[size]=20' \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/webhook-subscriptions?page[number]=1&page[size]=20",
{
method: "GET",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.mageloyalty.com/v1/webhook-subscriptions",
headers={"Authorization": ""},
params={"page[number]": 1, "page[size]": 20},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": [
{
"type": "webhook_subscription",
"id": "whsub_abc123",
"attributes": {
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"is_active": true,
"description": "Production points + tier sync",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
}
],
"meta": {
"total": 1,
"page": 1,
"per_page": 20,
"total_pages": 1
}
}
```
```json 401 Unauthorized theme={null}
{
"errors": [
{
"status": "401",
"title": "Unauthorized",
"detail": "Invalid or revoked API key"
}
]
}
```
# Webhook subscription object
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/object
The webhook subscription object defines where Mage Loyalty delivers event notifications.
A webhook subscription registers a URL that Mage Loyalty calls whenever one of your selected loyalty events happens. Each subscription has its own signing secret, used to verify that deliveries genuinely came from Mage Loyalty.
## Attributes
| Field | Type | Description |
| - | - | - |
| `url` | string | The HTTPS endpoint that receives webhook deliveries |
| `events` | string\[] | The events this subscription is subscribed to. One or more of `points.earned`, `points.redeemed`, `customer.tier_upgrade`, `customer.tier_downgrade` |
| `is_active` | boolean | Whether deliveries are currently sent to this subscription |
| `description` | string \| null | Optional label to help you identify the subscription |
| `secret` | string | The signing secret used to verify delivery signatures. Returned **only** when the subscription is created and when the secret is rotated, never on get, list, or update |
| `created_at` | string | ISO 8601 timestamp |
| `updated_at` | string | ISO 8601 timestamp |
The `secret` is shown once, at creation and on rotation. Store it securely. If you lose it, [rotate the secret](/api-reference/webhooks/subscriptions/rotate-secret) to get a new one. See [Verifying webhook signatures](/api-reference/webhooks/verifying-signatures) for how the secret is used.
## Example
```json theme={null}
{
"type": "webhook_subscription",
"id": "whsub_abc123",
"attributes": {
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"is_active": true,
"description": "Production points + tier sync",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
}
```
# Rotate a subscription's signing secret
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/rotate-secret
POST /v1/webhook-subscriptions/{id}/rotate-secret
Issue a new signing secret for a webhook subscription and invalidate the old one.
Issue a new signing secret for a webhook subscription. The previous secret is invalidated immediately, so any deliveries signed after rotation use the new secret. Use this if a secret may have been exposed, or if you lost the original.
The new `secret` is returned **only** in this response. It is shown once and never again. Store it securely before you finish handling the response. The old secret stops working as soon as rotation completes, so update your endpoint to verify with the new secret. See [Verifying webhook signatures](/api-reference/webhooks/verifying-signatures).
## Path parameters
| Parameter | Type | Description |
| - | - | - |
| `id` | string | The webhook subscription ID |
## Response
Returns `200 OK` with the subscription, including the new `secret`.
## Errors
| Status | Scenario |
| - | - |
| `401` | Missing or invalid API key |
| `403` | The API key is read-only and cannot perform write operations |
| `404` | No subscription with this ID exists for the shop |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request POST \
--url https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123/rotate-secret \
--header 'Authorization: '
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123/rotate-secret",
{
method: "POST",
headers: {
Authorization: "",
},
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123/rotate-secret",
headers={"Authorization": ""},
)
data = response.json()
```
```php PHP theme={null}
true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
],
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "webhook_subscription",
"id": "whsub_abc123",
"attributes": {
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "customer.tier_upgrade"],
"is_active": true,
"description": "Production points + tier sync",
"secret": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-03T00:00:00.000Z"
}
}
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Webhook subscription 'whsub_abc123' not found"
}
]
}
```
# Update a webhook subscription
Source: https://developers.mageloyalty.com/api-reference/webhooks/subscriptions/update
PUT /v1/webhook-subscriptions/{id}
Update the URL, events, active status, or description of a webhook subscription.
Update a webhook subscription. Send only the fields you want to change. The signing secret is not returned by this endpoint and is never changed here. To issue a new secret, use [rotate secret](/api-reference/webhooks/subscriptions/rotate-secret).
## Path parameters
| Parameter | Type | Description |
| - | - | - |
| `id` | string | The webhook subscription ID |
## Request body
All fields are optional. Include any subset you want to change.
| Field | Type | Description |
| - | - | - |
| `url` | string | A new HTTPS endpoint. Must be a valid URL |
| `events` | string\[] | A non-empty array of events. Valid events: `points.earned`, `points.redeemed`, `customer.tier_upgrade`, `customer.tier_downgrade` |
| `is_active` | boolean | Set to `false` to pause deliveries, `true` to resume |
| `description` | string \| null | A new label, or `null` to clear it |
## Errors
| Status | Scenario |
| - | - |
| `400` | `url` not a string, `url` not a valid URL, `events` empty, one or more events invalid, or invalid JSON |
| `401` | Missing or invalid API key |
| `403` | The API key is read-only and cannot perform write operations |
| `404` | No subscription with this ID exists for the shop |
| `429` | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second) |
```bash cURL theme={null}
curl --request PUT \
--url https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123 \
--header 'Authorization: ' \
--header 'Content-Type: application/json' \
--data '{
"events": ["points.earned", "points.redeemed"],
"is_active": false
}'
```
```javascript Node theme={null}
const response = await fetch(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123",
{
method: "PUT",
headers: {
Authorization: "",
"Content-Type": "application/json",
},
body: JSON.stringify({
events: ["points.earned", "points.redeemed"],
is_active: false,
}),
}
);
const data = await response.json();
```
```python Python theme={null}
import requests
response = requests.put(
"https://api.mageloyalty.com/v1/webhook-subscriptions/whsub_abc123",
headers={"Authorization": ""},
json={
"events": ["points.earned", "points.redeemed"],
"is_active": False,
},
)
data = response.json()
```
```php PHP theme={null}
"PUT",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"events" => ["points.earned", "points.redeemed"],
"is_active" => false,
]),
]);
$data = json_decode(curl_exec($ch), true);
```
```json 200 OK theme={null}
{
"data": {
"type": "webhook_subscription",
"id": "whsub_abc123",
"attributes": {
"url": "https://example.com/webhooks/mage",
"events": ["points.earned", "points.redeemed"],
"is_active": false,
"description": "Production points + tier sync",
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-02T00:00:00.000Z"
}
}
}
```
```json 400 Invalid events theme={null}
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "Invalid event(s): points.created. Valid events: points.earned, points.redeemed, customer.tier_upgrade, customer.tier_downgrade"
}
]
}
```
```json 404 Not Found theme={null}
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Webhook subscription 'whsub_abc123' not found"
}
]
}
```
# Verifying Webhook Signatures
Source: https://developers.mageloyalty.com/api-reference/webhooks/verifying-signatures
Confirm that incoming webhook requests are genuinely from Mage Loyalty and haven't been tampered with.
Every webhook request includes an `X-Webhook-Signature` header containing an HMAC-SHA256 signature. You **must** verify this signature before processing the payload — otherwise anyone who discovers your endpoint URL could send fake events.
## How it works
Mage Loyalty signs every webhook using your subscription's **signing secret**:
1. A signing string is created by concatenating the timestamp, a dot, and the raw request body: `{timestamp}.{rawBody}`
2. The signing string is hashed with HMAC-SHA256 using your signing secret as the key
3. The result is hex-encoded and prefixed with `sha256=`
Your server repeats the same process and compares the result.
## Step-by-step
Read `X-Webhook-Signature` and `X-Webhook-Timestamp` from the incoming request.
```
X-Webhook-Signature: sha256=a1b2c3d4e5f6...
X-Webhook-Timestamp: 2026-02-18T12:00:00.000Z
```
You need the **raw** body string exactly as it was received — not a parsed-and-re-serialized version. Most frameworks provide a way to access this.
If you parse the JSON body and then `JSON.stringify()` it, the output may differ from the original (different key ordering, whitespace). This will cause signature verification to fail. Always use the raw body.
Concatenate the timestamp, a literal `.` character, and the raw body:
```
2026-02-18T12:00:00.000Z.{"event":"points.earned","shop":"my-store",...}
```
Create an HMAC-SHA256 hash of the signing string using your signing secret, then hex-encode it and prepend `sha256=`.
Compare your computed signature with the `X-Webhook-Signature` header using a **timing-safe** comparison function. Do not use `===` or `==` — these are vulnerable to timing attacks.
If the two values match, the webhook is verified.
## Complete examples
```javascript theme={null}
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const rawBody = req.body; // raw request body string — NOT parsed JSON
// 1. Build the signing string
const signingString = timestamp + '.' + rawBody;
// 2. Compute the expected signature
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signingString)
.digest('hex');
// 3. Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
```
**Express example** — make sure to capture the raw body:
```javascript theme={null}
const express = require('express');
const app = express();
// Capture the raw body as a string alongside the parsed JSON
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhooks/loyalty', (req, res) => {
const isValid = verifyWebhook(
{ headers: req.headers, body: req.rawBody },
process.env.MAGE_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
console.log(`Received ${event}`, data);
res.status(200).send('OK');
});
```
```python theme={null}
import hmac
import hashlib
def verify_webhook(signature: str, timestamp: str, raw_body: str, secret: str) -> bool:
# 1. Build the signing string
signing_string = f"{timestamp}.{raw_body}"
# 2. Compute the expected signature
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
signing_string.encode("utf-8"),
hashlib.sha256
).hexdigest()
# 3. Timing-safe comparison
return hmac.compare_digest(signature, expected)
```
**Flask example:**
```python theme={null}
from flask import Flask, request, abort
app = Flask(__name__)
@app.route("/webhooks/loyalty", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Webhook-Signature", "")
timestamp = request.headers.get("X-Webhook-Timestamp", "")
raw_body = request.get_data(as_text=True)
if not verify_webhook(signature, timestamp, raw_body, MAGE_WEBHOOK_SECRET):
abort(401)
payload = request.get_json()
print(f"Received {payload['event']}", payload["data"])
return "OK", 200
```
```ruby theme={null}
require 'openssl'
def verify_webhook(signature, timestamp, raw_body, secret)
# 1. Build the signing string
signing_string = "#{timestamp}.#{raw_body}"
# 2. Compute the expected signature
digest = OpenSSL::HMAC.hexdigest('sha256', secret, signing_string)
expected = "sha256=#{digest}"
# 3. Timing-safe comparison
ActiveSupport::SecurityUtils.secure_compare(signature, expected)
end
```
```php theme={null}
function verifyWebhook(
string $signature,
string $timestamp,
string $rawBody,
string $secret
): bool {
// 1. Build the signing string
$signingString = $timestamp . '.' . $rawBody;
// 2. Compute the expected signature
$expected = 'sha256=' . hash_hmac('sha256', $signingString, $secret);
// 3. Timing-safe comparison
return hash_equals($expected, $signature);
}
```
## Replay attack protection (optional)
The `X-Webhook-Timestamp` header is included in the signed data, so it cannot be forged. You can optionally reject requests where the timestamp is too old:
```javascript theme={null}
const MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes
function isTimestampValid(timestamp) {
const age = Date.now() - new Date(timestamp).getTime();
return age >= 0 && age <= MAX_AGE_MS;
}
```
## Troubleshooting
* **Re-serialized body**: You parsed the JSON and re-stringified it. Use the raw body exactly as received.
* **Wrong secret**: Each webhook subscription has its own unique signing secret. Make sure you're using the correct one.
* **Middleware modified the body**: Some frameworks (e.g. Express without the `verify` option) consume the raw body during JSON parsing. Use the `verify` callback shown above to preserve it.
* **Encoding issues**: The raw body must be read as UTF-8. Binary or other encodings will produce a different hash.
Signing secrets are shown only once at creation time. If you've lost yours, go to **Settings > Webhooks**, edit the subscription, and click **Rotate signing secret**. A new secret will be generated and shown once. Update your server with the new secret — the old one is immediately invalidated.
Use a tunneling tool like [ngrok](https://ngrok.com) to expose your local server, then register the tunnel URL as your webhook endpoint. You can also use the **Test** button in the dashboard to send a test event.
# Introduction
Source: https://developers.mageloyalty.com/introduction
Welcome to the Mage Loyalty documentation
Check out our developer toolkit documentation for everything you need to get started:
Access and manage your loyalty data from any external app, tool, or backend service.
Access loyalty data on the storefront and build fully custom customer experiences.
Access loyalty data directly in your Liquid templates.
Receive real-time notifications when loyalty events occur.
# getCartEarningValue
Source: https://developers.mageloyalty.com/js-sdk/cart/get-cart-earning-value
Get the total points or store credit value the customer's current cart would earn.
Calculate what the current cart is worth in loyalty value. The SDK reads the cart from your storefront (via Shopify's `/cart.js`) and asks the Mage server to calculate the result, so the number always matches what the customer will actually earn at checkout. The calculation includes VIP tier multipliers, active bonus campaigns, discount exclusions from your purchase earning rule, and multi currency conversion.
Works for both guests and logged in customers. Guests see the base earning rate; logged in customers see their personalised rate, including any VIP tier boost.
A common use is a cart badge: "Earn 1,250 points with this order".
## Usage
```javascript theme={null}
MageSDK.getCartEarningValue().then(function(resp) {
if (resp.success) {
console.log(resp.data.formatted); // "1,250" or "$12.50"
}
});
```
### Example: cart badge
Call the method whenever your cart UI renders or updates, and drop `formatted` into your badge. Results are cached, so repeat calls for an unchanged cart are instant.
**Points mode:**
```javascript theme={null}
async function updateCartBadge() {
const resp = await MageSDK.getCartEarningValue();
if (!resp.success) return;
const badge = document.querySelector('#loyalty-cart-badge');
if (badge) {
// formatted is "1,250"
badge.textContent = `Earn ${resp.data.formatted} points with this order`;
}
}
updateCartBadge();
```
**Store credit mode:**
```javascript theme={null}
async function updateCartBadge() {
const resp = await MageSDK.getCartEarningValue();
if (!resp.success) return;
const badge = document.querySelector('#loyalty-cart-badge');
if (badge) {
// formatted is "$12.50"
badge.textContent = `Earn ${resp.data.formatted} in store credit with this order`;
}
}
updateCartBadge();
```
If you are unsure which mode a shop runs in, or you are building for both, check `resp.data.loyaltyMode` (`"points"` or `"store_credit"`) and pick the wording accordingly.
### Keeping the badge in sync
Run `updateCartBadge()` on page load, then again whenever the cart changes. How you detect cart changes depends on your theme and any cart apps you use, as there is no single event every Shopify storefront fires:
* Many themes dispatch a custom event after AJAX cart updates, commonly `cart:updated`, `cart:refresh`, or `ajaxProduct:added`. Check your theme's JavaScript for the event name it uses, then listen for it: `document.addEventListener('cart:updated', updateCartBadge)`.
* Cart drawer and upsell apps often re-render the cart themselves and may expose their own events or callbacks. Check the app's documentation.
* If your theme re-renders the cart section with the Section Rendering API, call `updateCartBadge()` from the same code that triggers the re-render.
* On a standard `/cart` page without AJAX updates, quantity changes reload the page, so running on page load is enough.
## Response
Whether the request was successful.
The total value the cart would earn, in the shop's loyalty unit. In points mode this is the number of points. In store credit mode this is the credit amount in minor units (for example `1250` is \$12.50). See [Loyalty modes](/js-sdk/loyalty-modes).
Display ready version of `points`, formatted in the shopper's locale. `"1,250"` in points mode, `"$12.50"` in store credit mode. In most cases this is the only field you need to render.
`"points"` or `"store_credit"`. Use it to choose the wording around the value, for example "points" versus "in store credit".
Error message when `success` is `false`.
## Notes
* Store credit is always earned in the shop's own currency. If a shopper browses your store in a different currency, the cart is converted before calculating, and `formatted` shows the shop currency value they will actually receive.
* An empty cart returns `points: 0`. If the shop has no active purchase earning rule, the value is also `0`; you may want to hide the badge in that case.
* The value is a preview. The actual award happens when the order is paid, using the same calculation.
# getCustomerActivity
Source: https://developers.mageloyalty.com/js-sdk/customers/get-customer-activity
Retrieve the logged-in customer's unified activity feed.
Retrieve a paginated, newest-first feed of the customer's loyalty activity: point earnings, redemptions, and cashback rows combined into a single list.
## Usage
```javascript theme={null}
MageSDK.getCustomerActivity({ limit: 20, offset: 0 }).then(function(resp) {
if (resp.success) {
resp.data.activity.forEach(function(row) {
var value = MageSDK.formatSignedLoyaltyValue(
row.points,
resp.data.loyaltyMode,
resp.data.currency
);
console.log(row.name, value);
});
}
});
```
## Parameters
Maximum number of rows to return.
Number of rows to skip, for pagination.
## Response
Whether the request was successful.
The customer's current balance, in the shop's loyalty unit.
When the next tranche of points expires, or `null`.
Whether the shop runs cashback. When `true`, cashback rows may appear in the feed.
The activity rows.
The row ID.
`"earning"`, `"redemption"`, or `"cashback"`.
A display name for the row.
The point value of the row (0 for cashback rows). Positive for earnings, representing the amount moved.
Amount later reversed, if any.
Row status, e.g. `"APPROVED"`, `"EXPIRED"`, `"PENDING"`, `"REFUNDED"`, `"PARTIALLY_REFUNDED"`.
ISO 8601 timestamp of the event.
Expiry timestamp, set only for future-dated points drops.
Cashback rows only. The money amount of the cashback.
Cashback rows only. Any clawed-back portion.
Cashback rows only. The currency symbol for `amount`.
`{ hasMore, totalCount }`. Use with `offset` to page through the feed.
Merchant-configured `{ greeting, description }` copy for the activity block.
`"points"` or `"store_credit"`. In store-credit mode `points` values are minor currency units. See [Loyalty modes](/js-sdk/loyalty-modes).
The shop's ISO currency code.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"points": 450,
"pointsNextExpiryAt": "2025-01-01T00:00:00.000Z",
"hasCashback": false,
"activity": [
{
"id": "cer_abc",
"type": "earning",
"name": "Points for purchase",
"points": 150,
"refundedPoints": 0,
"status": "APPROVED",
"createdAt": "2024-06-01T12:00:00.000Z",
"pointsExpiryAt": null
},
{
"id": "cr_abc",
"type": "redemption",
"name": "£5 off your next order",
"points": 500,
"refundedPoints": 0,
"status": "USED",
"createdAt": "2024-05-20T09:00:00.000Z",
"pointsExpiryAt": null
}
],
"pagination": {
"hasMore": false,
"totalCount": 2
},
"content": {
"greeting": "Your activity",
"description": "Everything you've earned and redeemed."
},
"loyaltyMode": "points",
"currency": "GBP"
}
}
```
# getCustomerDetails
Source: https://developers.mageloyalty.com/js-sdk/customers/get-customer-details
Retrieve details for the logged-in customer, including balance, VIP tier, and earning history.
Retrieve the full profile for the currently logged-in customer. Returns their loyalty balance, VIP tier, earning history, and referral earnings, along with the shop's loyalty mode and currency.
## Usage
```javascript theme={null}
MageSDK.getCustomerDetails().then(function(resp) {
if (resp.success) {
console.log(resp.data.customer);
console.log(resp.data.loyaltyMode); // 'points' | 'store_credit'
}
});
```
## Response
Whether the request was successful.
The shop's loyalty mode, `"points"` or `"store_credit"`. In store-credit mode every loyalty number below is in the currency's minor units. See [Loyalty modes](/js-sdk/loyalty-modes).
The shop's ISO currency code, e.g. `"USD"`.
The customer object.
Internal Mage customer ID.
The Shopify customer ID.
Customer email address.
Customer first name.
Customer last name.
Current available balance. Points in points mode, minor currency units in store-credit mode.
Total earned over the customer's lifetime, in the same unit as `points`.
Total redeemed, in the same unit as `points`.
Whether the customer is excluded from the loyalty program.
Customer date of birth in ISO 8601 format.
The customer's unique referral code.
The customer's full referral URL that can be shared.
The customer's current VIP tier, or `null` if no tier is assigned. See [getShopVipTiers](/js-sdk/vip-tiers/get-shop-vip-tiers) for the tier shape.
The customer's progress value toward the next tier, measured by the shop's primary entry method.
Progress toward the secondary tier-entry threshold, when the shop combines two entry methods. `null` otherwise.
List of the customer's point earning events, newest first.
The earning record ID.
ID of the earning rule that produced this entry.
Status of the entry, e.g. `"APPROVED"`. Expired rows are surfaced as `"EXPIRED"`.
Amount earned, in the shop's loyalty unit.
Amount later reversed (for example after an order refund).
The earning rule action, e.g. `"purchase"`, `"birthday"`, `"points_drop"`.
A display name for the row.
Expiry timestamp. Set only for future-dated points drops, `null` otherwise.
The related earning rule, `{ id, name }`, or `null`.
Backwards-compatible alias of `customerEarningRule`. New code should read `customerEarningRule`.
List of earnings from referrals.
In store-credit mode, a ready-to-display view of the balance (`balanceMinor`, `balance`, `balanceFormatted`, `currency`). `null` in points mode. See [Loyalty modes](/js-sdk/loyalty-modes).
ISO 8601 timestamp of when the customer was enrolled.
ISO 8601 timestamp of the last update.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"loyaltyMode": "points",
"currency": "GBP",
"customer": {
"id": "abc123",
"shopifyCustomerId": "6789012345",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"points": 450,
"lifetimePoints": 1200,
"redeemedPoints": 750,
"isExcluded": false,
"dateOfBirth": "1990-05-15T00:00:00.000Z",
"referralCode": "ABC123",
"referralUrl": "https://mystore.com?referral_code=ABC123",
"tier": {
"id": "tier_abc",
"name": "Gold",
"description": "Our most valued customers",
"threshold": 500,
"thresholdPoints": 500,
"thresholdSpend": null,
"thresholdOrders": null,
"pointsMultiplier": 1.5,
"badgeImageUrl": null,
"badgeColor": "#FFD700"
},
"tierProgressValue": 1200,
"tierProgressValueExtra": null,
"customerEarningRule": [
{
"id": "cer_abc",
"earningRuleId": "rule_abc",
"status": "APPROVED",
"points": 150,
"refundedPoints": 0,
"action": "purchase",
"name": "Points for purchase",
"pointsExpiryAt": null,
"earningRule": {
"id": "rule_abc",
"name": "Points for purchase"
}
}
],
"referralEarnings": [],
"storeCredit": null,
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-06-20T14:30:00.000Z"
}
}
}
```
# getCustomerVipTier
Source: https://developers.mageloyalty.com/js-sdk/customers/get-customer-vip-tier
Retrieve the current VIP tier for the logged-in customer.
Retrieve the VIP tier assigned to the currently logged-in customer, along with their progress value toward the next tier.
## Usage
```javascript theme={null}
MageSDK.getCustomerVipTier().then(function(resp) {
if (resp.success) {
console.log(resp.data.tier);
}
});
```
## Response
Whether the request was successful.
The customer's current VIP tier, or `null` if no tier is assigned.
The tier ID.
Display name of the tier (e.g. "Gold").
Description of the tier, or `null` if not set.
The tier ranking value. Reflects the requirement measured by the shop's primary entry method (points, spend, or orders).
Points earning multiplier for this tier.
URL of the tier badge image, or `null` if not set.
Hex color code for the tier badge, or `null` if not set.
The customer's current progress value toward the next tier.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"tier": {
"id": "tier_abc",
"name": "Gold",
"description": "Our most valued customers",
"threshold": 500,
"pointsMultiplier": 1.5,
"badgeImageUrl": null,
"badgeColor": "#FFD700"
},
"tierProgressValue": 1200
}
}
```
# updateCustomerDob
Source: https://developers.mageloyalty.com/js-sdk/customers/update-customer-dob
Update the date of birth for the logged-in customer.
Update the customer's date of birth. Returns `true` on success, `false` on failure.
## Usage
```javascript theme={null}
MageSDK.updateCustomerDob('1990-05-15').then(function(success) {
if (success) {
alert('Birthday updated!');
}
});
```
## Parameters
The date of birth in `YYYY-MM-DD` format.
## Response
Returns a `boolean`. `true` if the update was successful, `false` otherwise.
## Example: Birthday form
```html theme={null}
```
# getProfile / updateProfile
Source: https://developers.mageloyalty.com/js-sdk/customers/update-profile
Read and update the logged-in customer's name and marketing consent.
Read the customer's profile with `getProfile`, and update their name and marketing consent with `updateProfile`. Both return the standard wrapped response shape.
To update the customer's date of birth, use [updateCustomerDob](/js-sdk/customers/update-customer-dob).
## getProfile
```javascript theme={null}
MageSDK.getProfile().then(function(resp) {
if (resp.success) {
console.log(resp.data.profile.firstName, resp.data.profile.lastName);
}
});
```
### Response
Whether the request was successful.
The customer's profile.
Customer first name.
Customer last name.
Customer email address.
Whether the customer is subscribed to email marketing.
## updateProfile
```javascript theme={null}
MageSDK.updateProfile({
firstName: 'Jane',
lastName: 'Doe',
acceptsMarketing: true
}).then(function(resp) {
if (resp.success) {
console.log('Updated:', resp.data.profile);
} else {
console.log('Error:', resp.error);
}
});
```
### Parameters
The customer's first name.
The customer's last name.
Whether the customer opts in to email marketing.
### Response
Whether the update was successful.
The updated profile (`firstName`, `lastName`).
## Example: Profile form
```html theme={null}
```
# getEarningRules
Source: https://developers.mageloyalty.com/js-sdk/earning-rules/get-earning-rules
List all active, storefront-visible earning rules for the shop.
Retrieve the active earning rules a customer can see, with points already boosted for the logged-in customer's VIP tier and any active bonus campaign. Use this to display how customers earn points. Pass the logged-in customer through the app proxy and the response reflects their personalized earning rates; for guests it returns base rates.
## Usage
```javascript theme={null}
MageSDK.getEarningRules().then(function(resp) {
if (resp.success) {
resp.data.earningRules.forEach(function(rule) {
// `points` is the final award after tier and campaign boosts
console.log(rule.name, rule.points);
});
}
});
```
## Response
Whether the request was successful.
List of active earning rules.
The earning rule ID.
Internal name of the rule.
Description of how points are earned, reflecting the boosted value.
The trigger action (e.g. `"purchase"`, `"birthday"`, `"follow_on_instagram"`).
Rule category (e.g. `"purchase"`, `"social"`, `"engagement"`).
Whether the rule is currently active.
The final award after tier and campaign boosts. Display this value.
The unboosted base award configured for the rule.
The configured base award (same source as `basePoints`).
The VIP tier multiplier applied to this rule for the customer, or `null` if no tier boost applies.
The active bonus-campaign multiplier applied to this rule, or `null` if none.
Points awarded for a photo review, when the rule supports reviews.
Points awarded for a video review, when the rule supports reviews.
Label to display to customers.
Configured call-to-action URL, or `null`.
The resolved, ready-to-use call-to-action URL.
The customer's specific earning record for this rule, or `null` if they haven't triggered it.
`"points"` or `"store_credit"`. In store-credit mode award values are minor currency units. See [Loyalty modes](/js-sdk/loyalty-modes).
The shop's ISO currency code.
Banner context when the customer's tier boosts earning, or `null`.
The tier's earning multiplier.
The tier name, e.g. `"Gold"`.
The multiplier mode.
Banner context for an active bonus campaign (`{ name, id }`), or `null`.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"earningRules": [
{
"id": "rule_abc",
"name": "Points for purchase",
"description": "Earn 15 points per £1 spent",
"action": "purchase",
"category": "purchase",
"isActive": true,
"points": 15,
"basePoints": 10,
"pointsToGive": 10,
"tierBoost": 1.5,
"campaignBoost": null,
"photoReviewPoints": null,
"videoReviewPoints": null,
"customerFacingLabel": "Earn points on every order",
"callToActionUrl": null,
"fullCallToActionUrl": null,
"customerEarningRule": null
}
],
"loyaltyMode": "points",
"currency": "GBP",
"tierInfo": {
"multiplier": 1.5,
"name": "Gold",
"mode": "all"
},
"campaignInfo": null
}
}
```
# Introduction
Source: https://developers.mageloyalty.com/js-sdk/introduction
The Mage JavaScript SDK gives you client-side access to your loyalty program from any Shopify storefront.
## JavaScript SDK
The Mage JavaScript SDK is a lightweight client-side library that lets you build custom loyalty experiences directly in your storefront. Once enabled, the SDK is bundled into your storefront and available as `window.MageSDK`.
## Enable the SDK
Enable the Mage JavaScript SDK from the merchant dashboard under **Settings > Developer**. Once enabled, the SDK is automatically injected into your storefront.
## Initialization
The SDK loads asynchronously. Use the `mage-sdk-loaded` event to ensure it's ready before calling any methods:
```javascript theme={null}
if (window.MageSDK) {
// MageSDK is loaded and ready to use
} else {
document.addEventListener('mage-sdk-loaded', function() {
// MageSDK is loaded and ready to use
});
}
```
## Response format
Most methods return a consistent response shape:
**Success:**
```json theme={null}
{
"success": true,
"data": { ... }
}
```
**Error:**
```json theme={null}
{
"success": false,
"error": "Customer not logged in"
}
```
`updateCustomerDob` is the one exception. It returns a plain `boolean` rather than the wrapped response shape, for backwards compatibility.
## Loyalty modes
A shop runs in one of two loyalty modes, set by the merchant:
* **`points`** The loyalty balance is plain points, so `450` means 450 points.
* **`store_credit`** The balance is Mage store credit, denominated in money. Every loyalty number the SDK returns (points, `pointsCost`, balances) is in the currency's minor units, so `450` means `$4.50`.
Methods that carry loyalty amounts also return `loyaltyMode` and `currency`. Pass both to the built-in [`formatLoyaltyValue`](/js-sdk/loyalty-modes) helper and it renders correctly in either mode, so your code stays mode-agnostic. See the [Loyalty modes](/js-sdk/loyalty-modes) guide for the full picture, including how redemption differs.
## Common errors
| Error | Cause |
| - | - |
| `"Unauthorized"` | No Shopify session (customer not on the store) |
| `"Customer not found"` | Customer is not logged in or not enrolled |
| `"Customer is excluded from the loyalty program"` | Customer has been excluded by the merchant |
| `"This reward cannot be redeemed in store credit mode"` | Fixed-cost redemption attempted on a store-credit shop. Use [`redeemFlexible`](/js-sdk/rewards/redeem-flexible) instead |
| `"Network error"` | Failed to reach the API |
## Available methods
Customer details, VIP tier, activity feed, and profile updates
List rewards, view redeemed rewards, and redeem points or store credit
List all VIP tier configurations
List active earning rules with tier and campaign boosts
Referral stats, links, and social sharing
Format and redeem correctly in points and store-credit shops
Programmatically open and close the loyalty sidebar
# Loyalty modes
Source: https://developers.mageloyalty.com/js-sdk/loyalty-modes
Every shop runs on either points or store credit. How to detect the mode, format values, and redeem correctly in both.
Every Mage shop runs in one of two loyalty modes, chosen by the merchant:
* **`points`** The loyalty balance is plain points.
* **`store_credit`** The loyalty balance is Mage store credit, denominated in money.
The loyalty engine and every SDK method behave identically in both modes. The SDK returns the same fields either way. What changes is how you **interpret and display** the numbers, and, for redemption, which method a customer uses.
Store credit here means Mage's internal store credit, not Shopify native store credit.
## How each mode denominates values
Every loyalty number the SDK returns (`points`, `lifetimePoints`, `redeemedPoints`, a reward's `pointsCost`, an earning rule's award) is read in the shop's loyalty unit:
| | `points` mode | `store_credit` mode |
| - | - | - |
| Unit | Whole points | Currency minor units |
| `points: 450` means | 450 points | 4.50 in the shop currency (or 450 for zero-decimal currencies like JPY/KRW) |
| Displayed as | `"450"` | `"$4.50"` |
| Redemption | [`redeemReward`](/js-sdk/rewards/redeem-reward) or [`redeemFlexible`](/js-sdk/rewards/redeem-flexible) | [`redeemFlexible`](/js-sdk/rewards/redeem-flexible) only |
Because the raw numbers look the same in both modes, never assume points. Read the mode and format through the helpers below.
## Detecting the mode
Methods that carry loyalty amounts return `loyaltyMode` and `currency` alongside their data. These are `getCustomerDetails`, `getCustomerActivity`, `getShopRewards`, `getEarningRules`, and `getShopVipTiers`.
```javascript theme={null}
MageSDK.getCustomerDetails().then(function(resp) {
if (!resp.success) return;
var customer = resp.data.customer;
var mode = resp.data.loyaltyMode; // 'points' | 'store_credit'
var currency = resp.data.currency; // e.g. 'USD'
var balance = MageSDK.formatLoyaltyValue(customer.points, mode, currency);
document.getElementById('balance').textContent = balance;
// points mode -> "450"
// credit mode -> "$4.50"
});
```
The same `formatLoyaltyValue` call handles both modes, so your rendering code stays mode-agnostic. Pass the mode and currency straight through and let the helper decide.
## Formatting helpers
The SDK exposes helpers on `window.MageSDK` so you never reimplement the unit math. Each one works in both modes.
Formats a loyalty value for display. Points mode returns the number with locale grouping (`"1,250"`). Store-credit mode returns a localized currency string (`"$12.50"`).
Same as `formatLoyaltyValue` but prefixes a `+` or `-` sign. Useful for activity rows (`"+150"`, `"+$3.00"`, `"-300"`).
Returns `"point"` or `"points"` in points mode (based on `count`), and an empty string in store-credit mode, where the money value already describes itself.
Normalizes any input to `'points'` or `'store_credit'`. Anything that isn't exactly `'store_credit'` resolves to `'points'`, so an older server or a missing value safely defaults to points.
Convenience check. Returns `true` when the resolved mode is `'store_credit'`.
Converts a minor-unit amount to major units (`300` becomes `3` for USD, `500` stays `500` for JPY). Relevant in store-credit mode when you need the raw number. For display, prefer `formatLoyaltyValue`.
```javascript theme={null}
// Points mode
MageSDK.formatLoyaltyValue(1250, 'points', 'USD'); // "1,250"
MageSDK.formatSignedLoyaltyValue(150, 'points', 'USD'); // "+150"
MageSDK.loyaltyUnitLabel('points', 1); // "point"
// Store-credit mode
MageSDK.formatLoyaltyValue(1250, 'store_credit', 'USD'); // "$12.50"
MageSDK.formatLoyaltyValue(500, 'store_credit', 'JPY'); // "¥500"
MageSDK.formatSignedLoyaltyValue(-300, 'store_credit', 'USD'); // "-$3.00"
MageSDK.isStoreCredit('store_credit'); // true
```
## The storeCredit convenience object
In store-credit mode, `getCustomerDetails` attaches a ready-to-display `storeCredit` object to the customer. In points mode it is `null`, so you can branch on its presence.
```json theme={null}
{
"storeCredit": {
"balanceMinor": 450,
"balance": "4.50",
"balanceFormatted": "$4.50",
"currency": "USD"
}
}
```
The raw balance in minor units. Identical to `customer.points` in store-credit mode.
The balance in major units as a decimal string, e.g. `"4.50"`.
A localized display string, e.g. `"$4.50"`.
The ISO currency code.
## Redemption differs by mode
Redemption is the one behavior that is not identical across modes.
**Points mode** supports both redemption methods:
* [`redeemReward`](/js-sdk/rewards/redeem-reward) for `fixed` rewards (a set cost for a set discount).
* [`redeemFlexible`](/js-sdk/rewards/redeem-flexible) for `flexible` rewards (the customer chooses how much to spend).
**Store-credit mode** supports only flexible redemption. Calling `redeemReward` there returns:
```json theme={null}
{
"success": false,
"error": "This reward cannot be redeemed in store credit mode"
}
```
So in store-credit shops, redeem through [`redeemFlexible`](/js-sdk/rewards/redeem-flexible). A reward is redeemable this way when its `redemptionMode` is `"flexible"`; read the amount bounds from the reward's `flexibleConfig` (`minPoints` / `maxPoints`).
```javascript theme={null}
MageSDK.getShopRewards().then(function(resp) {
if (!resp.success) return;
var flexible = resp.data.regularRewards.filter(function(r) {
return r.redemptionMode === 'flexible';
});
var reward = flexible[0];
if (!reward) return;
// Spend the customer's chosen amount (here, the configured minimum)
var amount = reward.flexibleConfig.minPoints;
MageSDK.redeemFlexible(reward.id, amount).then(function(res) {
if (res.success) {
console.log('Discount code:', res.data.customerReward.discountCode);
console.log('Value:', res.data.discountAmount);
} else {
console.log('Error:', res.error);
}
});
});
```
If you want redemption code that works regardless of mode, drive it off the reward's `redemptionMode`: call `redeemReward` for `"fixed"` rewards and `redeemFlexible` for `"flexible"` rewards. Store-credit shops only surface flexible rewards, so this pattern naturally does the right thing in both modes.
# getProductBonusCampaign
Source: https://developers.mageloyalty.com/js-sdk/product/get-product-bonus-campaign
Get the active bonus campaign for a product and when it ends.
Returns the bonus campaign running on a product, such as double points this weekend. Returns `null` when there is no active campaign for the product, including once a campaign has ended, so the same check both shows and hides your notice.
## Usage
```javascript theme={null}
const resp = await MageSDK.getProductBonusCampaign({ productId: {{ product.id }} });
if (resp.success && resp.data) {
console.log(resp.data.multiplier); // 2
console.log(resp.data.endsAt); // "2026-10-21T03:59:59.000Z"
}
```
### Parameters
The product ID.
### Example
```javascript theme={null}
const resp = await MageSDK.getProductBonusCampaign({ productId });
const campaign = resp.success ? resp.data : null;
if (campaign && campaign.type === 'multiplier') {
pillEl.textContent = `${campaign.multiplier}× points this weekend`;
const ends = new Date(campaign.endsAt).toLocaleDateString(undefined, {
day: 'numeric',
month: 'short',
timeZone: 'America/New_York' // your store's timezone
});
endsEl.textContent = `Ends ${ends}`;
noticeEl.hidden = false;
} else {
// No campaign, or it has ended
noticeEl.hidden = true;
}
```
### When the campaign ends
Once `endsAt` passes, `data` is `null` on every call, even from a page that was already open, so re-running the check hides the notice. To hide it at the exact moment without re-calling, compare `endsAt` to the current time on a timer, as the [Bonus campaign countdown](/js-sdk/recipes/bonus-campaign-countdown) recipe does.
`endsAt` is in UTC. Pass your store's timezone when formatting a date, otherwise customers see it in their own timezone.
For a live countdown ("Ends in 2d 14h") with a campaign pill, see the [Bonus campaign countdown](/js-sdk/recipes/bonus-campaign-countdown) recipe.
To show how much the customer earns with the bonus, use [getProductEarningValue](/js-sdk/product/get-product-earning-value). Only show bonus messaging when its `bonusApplied` is `true`.
## Response
`data` is `null` when no active campaign applies to the product: none is running, the product isn't part of it, or the campaign has ended.
`"multiplier"` (for example double points) or `"tiered"` (a bonus for spending over an amount).
The campaign name.
Multiplier campaigns only. For example `2` for double points.
Tiered campaigns only. Each tier has `spendFormatted` (`"$500.00"`) and `bonusFormatted` (`"$25.00"` or `"500"`), plus the raw numbers `spendThreshold` and `bonusValue`.
When the campaign ends, as an ISO 8601 date in UTC.
Set when the campaign only applies if a specific product is in the cart: `{ met, productIds }`, where `met` is `true` once one of `productIds` is in the cart. Otherwise `null`.
Error message when `success` is `false`.
```json Multiplier theme={null}
{
"success": true,
"data": {
"type": "multiplier",
"name": "Double Points Weekend",
"multiplier": 2,
"endsAt": "2026-10-21T03:59:59.000Z",
"requiredProducts": null
}
}
```
```json Tiered (points) theme={null}
{
"success": true,
"data": {
"type": "tiered",
"name": "Spend More, Earn More",
"tiers": [
{ "spendThreshold": 100, "spendFormatted": "$100.00", "bonusValue": 500, "bonusFormatted": "500" },
{ "spendThreshold": 250, "spendFormatted": "$250.00", "bonusValue": 1500, "bonusFormatted": "1,500" }
],
"endsAt": "2026-10-21T03:59:59.000Z",
"requiredProducts": null
}
}
```
```json Tiered (store credit) theme={null}
{
"success": true,
"data": {
"type": "tiered",
"name": "Spend More, Earn More",
"tiers": [
{ "spendThreshold": 100, "spendFormatted": "$100.00", "bonusValue": 5, "bonusFormatted": "$5.00" },
{ "spendThreshold": 250, "spendFormatted": "$250.00", "bonusValue": 15, "bonusFormatted": "$15.00" }
],
"endsAt": "2026-10-21T03:59:59.000Z",
"requiredProducts": null
}
}
```
```json No campaign or ended theme={null}
{
"success": true,
"data": null
}
```
# getProductEarningValue
Source: https://developers.mageloyalty.com/js-sdk/product/get-product-earning-value
Get the points or store credit a product earns, with and without the active bonus campaign.
Returns the points or store credit the current customer earns on a product. Use it for product page messages like "Earn 500 points with this purchase".
* **Bonus campaigns:** when a bonus campaign is boosting the product, it also returns what the customer would normally earn, for messages like "Usually 250 points".
## Usage
```javascript theme={null}
const resp = await MageSDK.getProductEarningValue({
productId: {{ product.id }},
variantId: {{ product.selected_or_first_available_variant.id }},
price: {{ product.selected_or_first_available_variant.price }}
});
if (resp.success) {
console.log(resp.data.formatted); // "500" or "$140.00"
console.log(resp.data.regularFormatted); // "250" or "$70.00"
}
```
### Parameters
The product ID.
The selected variant ID.
The selected variant's price in cents, as Liquid's `{{ variant.price }}` outputs it. Optional, but makes the call faster.
The quantity to calculate for.
### Example
```javascript theme={null}
const resp = await MageSDK.getProductEarningValue({ productId, variantId, price });
if (!resp.success) return;
const { formatted, regularFormatted, bonusApplied, loyaltyMode } = resp.data;
const unit = loyaltyMode === 'store_credit' ? 'in store credit' : 'points';
earnEl.textContent = `Earn ${formatted} ${unit} with this purchase`;
usuallyEl.textContent = bonusApplied ? `Usually ${regularFormatted}` : '';
```
Call it again when the customer changes variant or quantity.
## Response
What the customer earns, including any bonus. Points in points mode (`500`), or the store credit amount in store credit mode (`140` for \$140.00).
`value` ready to display: `"500"` or `"$140.00"`.
What the customer would normally earn, without the bonus. Equal to `value` when no bonus applies.
`regularValue` ready to display.
`true` when a bonus campaign is increasing what this customer earns on this product. Use it to decide whether to show bonus messaging.
`"points"` or `"store_credit"`. Use it to choose your wording.
Error message when `success` is `false`. Hide your message in that case.
```json Points theme={null}
{
"success": true,
"data": {
"value": 250,
"formatted": "250",
"regularValue": 250,
"regularFormatted": "250",
"bonusApplied": false,
"loyaltyMode": "points"
}
}
```
```json Store credit theme={null}
{
"success": true,
"data": {
"value": 70,
"formatted": "$70.00",
"regularValue": 70,
"regularFormatted": "$70.00",
"bonusApplied": false,
"loyaltyMode": "store_credit"
}
}
```
```json Bonus campaign (Points) theme={null}
{
"success": true,
"data": {
"value": 500,
"formatted": "500",
"regularValue": 250,
"regularFormatted": "250",
"bonusApplied": true,
"loyaltyMode": "points"
}
}
```
```json Bonus campaign (Store credit) theme={null}
{
"success": true,
"data": {
"value": 140,
"formatted": "$140.00",
"regularValue": 70,
"regularFormatted": "$70.00",
"bonusApplied": true,
"loyaltyMode": "store_credit"
}
}
```
# Bonus campaign countdown
Source: https://developers.mageloyalty.com/js-sdk/recipes/bonus-campaign-countdown
Show a bonus campaign badge and a live countdown on your product pages.
Show customers that a product earns bonus points or store credit right now, and how long they have left. This recipe renders two badges on the product page:
* A campaign pill, such as **2× points this weekend**
* A countdown, such as **Ends in 2d 14h**, that updates every minute
Both badges only appear while an active campaign covers the product, and disappear when it ends.
## 1. Add the markup
Place this where you want the badges to appear in your product template, for example above the product image or next to the price:
```html theme={null}
```
The `hidden` attribute keeps the badges out of sight until a campaign is found. If your theme CSS sets `display` on `.bonus-campaign`, add `[hidden] { display: none !important; }` so the attribute still wins.
## 2. Add the script
Add this to your product template (or your theme's JavaScript). Replace the `{{ product.id }}` Liquid tag if you render the product ID another way.
```html theme={null}
```
Style the two elements to match your theme; the `bonus-campaign__pill` and `bonus-campaign__timer` classes are there for that.
## Showing the boosted amount
To add a line like "Earn 500 points with this purchase, usually 250", pair this recipe with [getProductEarningValue](/js-sdk/product/get-product-earning-value). Only show the "usually" wording when its `bonusApplied` is `true`.
## Notes
* The pill text is yours to write. `campaign.multiplier` gives you the number; the campaign `name` from the Mage admin is also available if you prefer to display that.
* The countdown is based on `endsAt`, which is a UTC timestamp, so it is correct for every customer regardless of their timezone.
* When the campaign has a required product (see `requiredProducts`), you may want to change the wording until it is in the cart, for example "Add the gift box to unlock 2× points".
# getCustomerReferralStats
Source: https://developers.mageloyalty.com/js-sdk/referrals/get-customer-referral-stats
Get referral statistics for the logged-in customer.
Retrieve referral statistics for the currently logged-in customer, including a summary of referral statuses and individual referral details.
## Usage
```javascript theme={null}
MageSDK.getCustomerReferralStats().then(function(resp) {
if (resp.success) {
console.log('Total referrals:', resp.data.summary.totalReferrals);
console.log('Completed:', resp.data.summary.completed);
}
});
```
## Response
Whether the request was successful.
Aggregated referral statistics.
Total number of referrals made.
Referrals that haven't been acted on yet.
Referrals where the friend has signed up but not yet completed the required action.
Fully completed referrals.
Referrals that have expired.
Number of unclaimed referral rewards.
List of individual referrals.
The referral ID.
Email address of the referred friend.
Current status (`"pending"`, `"friend_converted"`, `"completed"`, `"expired"`).
ISO 8601 timestamp of when the referral was created.
When the referred friend placed their qualifying order, or `null`.
The advocate's reward discount code, once earned, or `null`.
Whether the advocate has used their reward code.
When the advocate's reward code expires, or `null`.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"summary": {
"totalReferrals": 5,
"pending": 2,
"friendConverted": 1,
"completed": 2,
"expired": 0,
"availableRewards": 1
},
"referrals": [
{
"id": "ref_abc",
"friendEmail": "friend@example.com",
"status": "completed",
"createdAt": "2024-06-01T12:00:00.000Z",
"friendOrderDate": "2024-06-03T09:15:00.000Z",
"referrerDiscountCode": "MAGE-REF-XYZ",
"referrerDiscountUsed": false,
"referrerDiscountExpiresAt": "2024-09-03T09:15:00.000Z"
}
]
}
}
```
# getReferralLink
Source: https://developers.mageloyalty.com/js-sdk/referrals/get-referral-link
Get the customer's referral link and sharing options.
Retrieve the referral link for the currently logged-in customer, along with social sharing configuration.
## Usage
```javascript theme={null}
MageSDK.getReferralLink().then(function(resp) {
if (resp.success && resp.data.enabled) {
console.log('Referral URL:', resp.data.referralUrl);
console.log('Referral code:', resp.data.referralCode);
}
});
```
## Response
Whether the request was successful.
Whether the referral program is enabled for this shop.
The customer's unique referral code.
Full referral URL that can be shared.
Quick referral statistics.
Total number of people referred.
Number of completed referrals.
Number of pending referrals.
Social sharing configuration set by the merchant.
Whether Twitter/X sharing is enabled.
Pre-filled message for Twitter/X shares.
Whether Facebook sharing is enabled.
Whether email sharing is enabled.
Whether WhatsApp sharing is enabled.
Pre-filled message for WhatsApp shares.
Whether SMS sharing is enabled.
Pre-filled message for SMS shares.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"enabled": true,
"referralCode": "ABC123",
"referralUrl": "https://mystore.com?referral_code=ABC123",
"stats": {
"totalReferred": 5,
"completedReferrals": 2,
"pendingReferrals": 2
},
"socialSharing": {
"twitter": true,
"twitterMessage": "Check out this store!",
"facebook": true,
"email": true,
"whatsapp": true,
"whatsappMessage": "Check out this store!",
"sms": false,
"smsMessage": null
}
}
}
```
## Example: Share buttons
```html theme={null}
```
# getCustomerRewards
Source: https://developers.mageloyalty.com/js-sdk/rewards/get-customer-rewards
List the logged-in customer's redeemed rewards and discount codes.
Retrieve all rewards that the currently logged-in customer has redeemed, including their discount codes and statuses.
## Usage
```javascript theme={null}
MageSDK.getCustomerRewards().then(function(resp) {
if (resp.success) {
resp.data.customerRewards.forEach(function(reward) {
console.log(reward.discountCode, reward.status);
});
}
});
```
## Response
Whether the request was successful.
List of redeemed rewards.
The customer reward ID.
Name of the reward that was redeemed.
Description of the reward.
Points spent to redeem this reward.
The generated Shopify discount code.
Type of discount (e.g. `"fixed"`, `"percentage"`), or `null`.
A formatted display value such as `"$10 off"` or `"15% off"`, or `null`.
Current status of the reward (`"ACTIVE"`, `"USED"`, `"EXPIRED"`).
ISO 8601 timestamp of when the reward was redeemed.
ISO 8601 timestamp of when the discount code expires, or `null` if it doesn't expire.
ISO 8601 timestamp of when the code was used, or `null` if unused.
Product image URL for free-product rewards, or `null`.
Variants the discount is restricted to, when applicable.
`true` for a standard redeemed reward.
`true` when the reward came from being referred (the friend's discount).
`true` when the reward came from referring someone (the advocate's reward).
## Sample response
```json theme={null}
{
"success": true,
"data": {
"customerRewards": [
{
"id": "cr_abc123",
"rewardName": "\u00a35 off your next order",
"rewardDescription": "\u00a35 off your order",
"points": 500,
"discountCode": "MAGE-XYZ789",
"discountType": "fixed",
"discountValue": "£5 off",
"status": "ACTIVE",
"createdAt": "2024-06-01T12:00:00.000Z",
"expiresAt": "2024-07-01T12:00:00.000Z",
"discountCodeRedeemedAt": null,
"isCustomerReward": true
}
]
}
}
```
# getShopRewards
Source: https://developers.mageloyalty.com/js-sdk/rewards/get-shop-rewards
List all available rewards for the shop.
Retrieve all rewards configured for the shop, split into regular rewards and tiered rewards (rewards locked behind VIP tiers). The response also carries the shop's loyalty mode and currency so you can format costs correctly.
## Usage
```javascript theme={null}
MageSDK.getShopRewards().then(function(resp) {
if (resp.success) {
console.log(resp.data.regularRewards);
console.log(resp.data.tieredRewards);
}
});
```
## Response
Whether the request was successful.
Rewards available to all customers.
The reward ID.
Display name of the reward.
Description of what the reward provides.
Requirement lines to render under the description, each `{ type, text }` (for example a minimum spend note).
Cost to redeem, in the shop's loyalty unit. For flexible rewards this is the starting or minimum cost.
Reward category (e.g. `"fixed"`, `"percentage"`, `"free_shipping"`).
Type of discount applied.
Raw discount value from the reward (fixed amount or percentage), or `null` for types without a scalar value.
URL of the reward icon, or `null`.
`"fixed"` (spend a set cost) or `"flexible"` (the customer chooses an amount). Flexible rewards redeem through [redeemFlexible](/js-sdk/rewards/redeem-flexible).
Present for flexible rewards. Defines the redemption bounds.
Cash discount produced per `perPoints` spent.
The points unit the `conversionRate` applies to.
Minimum amount the customer can redeem.
Maximum amount the customer can redeem.
`"ONE_TIME"`, `"SUBSCRIPTION"`, or `"BOTH"`. Gates whether the reward can apply to a subscription.
For subscription discounts, the number of renewals the discount applies to. `null` means all renewals.
VIP tier required to redeem, or `null` if available to all.
Formatted minimum spend required to use the discount.
Product or collection metadata for free-product rewards, or `null` for non-product rewards.
Rewards restricted to specific VIP tiers. Same shape as `regularRewards`.
The shop's currency symbol, e.g. `"$"`.
The subscription app powering "apply to subscription", or `null` when none is connected.
`"points"` or `"store_credit"`. In store-credit mode `pointsCost` values are minor currency units. See [Loyalty modes](/js-sdk/loyalty-modes).
The shop's ISO currency code.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"regularRewards": [
{
"id": "reward_abc",
"name": "£5 off your next order",
"description": "£5 off your order",
"descriptionRequirements": [],
"pointsCost": 500,
"category": "fixed",
"discountType": "fixed",
"discountAmount": 5,
"iconUrl": null,
"redemptionMode": "fixed",
"purchaseType": "ONE_TIME",
"recurringCycleLimit": null,
"flexibleConfig": null,
"tier": null,
"minimumSpend": "£20"
}
],
"tieredRewards": [],
"currencySymbol": "£",
"subscriptionsProvider": null,
"loyaltyMode": "points",
"currency": "GBP"
}
}
```
# redeemFlexible
Source: https://developers.mageloyalty.com/js-sdk/rewards/redeem-flexible
Redeem a flexible reward where the customer chooses how much to spend.
Redeem a `flexible` reward, where the customer chooses how much of their balance to spend and the server generates a discount for that amount. This powers "choose your own discount" rewards in points mode, and it is the only redemption path on store-credit shops.
## Usage
```javascript theme={null}
MageSDK.redeemFlexible('reward_abc', 300).then(function(resp) {
if (resp.success) {
console.log('Discount code:', resp.data.customerReward.discountCode);
console.log('Value:', resp.data.discountAmount);
} else {
console.log('Error:', resp.error);
}
});
```
## Parameters
The ID of a flexible reward (one whose `redemptionMode` is `"flexible"`). Get reward IDs from [`getShopRewards`](/js-sdk/rewards/get-shop-rewards).
The amount to spend, in the shop's loyalty unit (points, or minor currency units in store-credit mode). Must fall within the reward's `flexibleConfig.minPoints` and `flexibleConfig.maxPoints`.
## Response
Whether the redemption was successful.
The created reward record.
The customer reward ID.
The generated Shopify discount code.
The cash value of the generated discount, in major currency units.
The amount deducted from the customer's balance.
The customer's remaining balance after redemption.
## Errors
| Error | Cause |
| - | - |
| `"Customer not found"` | Customer is not logged in or not enrolled |
| `"Insufficient points"` | Customer does not have enough balance |
| `"Reward not found"` | The reward ID is invalid, or the reward is not flexible |
## Sample response
```json theme={null}
{
"success": true,
"data": {
"customerReward": {
"id": "cr_def456",
"discountCode": "MAGE-FLEX-ABC"
},
"discountAmount": 3,
"pointsRedeemed": 300,
"updatedPoints": 150
}
}
```
## Example: Choose-your-own-amount slider
```html theme={null}
```
# redeemReward
Source: https://developers.mageloyalty.com/js-sdk/rewards/redeem-reward
Redeem a fixed-cost reward for the logged-in customer.
Redeem a fixed-cost reward for the currently logged-in customer. Deducts the reward's cost from their balance and generates a Shopify discount code.
This method is for `fixed` rewards. For `flexible` rewards (where the customer chooses an amount) use [redeemFlexible](/js-sdk/rewards/redeem-flexible). On store-credit shops fixed-cost redemption is disabled, so `redeemFlexible` is the only redemption path. See [Loyalty modes](/js-sdk/loyalty-modes).
## Usage
```javascript theme={null}
MageSDK.redeemReward('reward_abc').then(function(resp) {
if (resp.success) {
console.log('Discount code:', resp.data.discountCode);
} else {
console.log('Error:', resp.error);
}
});
```
## Parameters
The ID of the reward to redeem. You can get reward IDs from [`getShopRewards`](/js-sdk/rewards/get-shop-rewards).
## Response
Whether the redemption was successful.
The generated Shopify discount code.
A summary of the redeemed reward.
Name of the reward.
Type of discount applied, or `null`.
The full customer reward record that was created, including `id`, `status`, `expiresAt`, and the discount code. Same shape as an entry from [`getCustomerRewards`](/js-sdk/rewards/get-customer-rewards).
## Errors
| Error | Cause |
| - | - |
| `"Customer not found"` | Customer is not logged in or not enrolled |
| `"Insufficient points"` | Customer does not have enough balance |
| `"Reward not found"` | The reward ID is invalid |
| `"This reward cannot be redeemed in store credit mode"` | The shop runs on store credit. Use [redeemFlexible](/js-sdk/rewards/redeem-flexible) |
## Example: Redeem button
```html theme={null}
```
# closeSidebar
Source: https://developers.mageloyalty.com/js-sdk/sidebar/close-sidebar
Programmatically close the Mage loyalty sidebar.
Close the Mage loyalty sidebar.
## Usage
```javascript theme={null}
MageSDK.closeSidebar();
```
## Parameters
None.
## Returns
`void`. This method does not return a value.
# openSidebar
Source: https://developers.mageloyalty.com/js-sdk/sidebar/open-sidebar
Programmatically open the Mage loyalty sidebar.
Open the Mage loyalty sidebar. Use this to trigger the sidebar from custom buttons or events on your storefront.
## Usage
```javascript theme={null}
MageSDK.openSidebar();
```
## Parameters
None.
## Returns
`void`. This method does not return a value.
## Example: Custom loyalty button
```html theme={null}
```
## Example: Open after earning points
```javascript theme={null}
// After a customer completes an action, open the sidebar to show their updated points
MageSDK.openSidebar();
```
# TypeScript Support
Source: https://developers.mageloyalty.com/js-sdk/typescript
Add type definitions for the Mage SDK in your TypeScript project.
The Mage SDK is injected into the storefront at runtime via `window.MageSDK`. Since it isn't an npm package, TypeScript won't know about it by default. You can add full type support by creating a declaration file in your project.
## Setup
Create a file called `mage-sdk.d.ts` anywhere in your project (e.g. the root, or a `types/` folder):
```typescript mage-sdk.d.ts theme={null}
interface MageSDKResponse {
success: boolean;
error?: string;
data?: T;
}
// 'store_credit' shops denominate every loyalty NUMBER (points, pointsCost,
// balances) in ISO minor units. See the Loyalty modes guide.
type MageLoyaltyMode = 'points' | 'store_credit';
// Computed by the SDK (not sent by the server) in store-credit mode.
interface MageStoreCreditBalance {
balanceMinor: number;
balance: string; // major-unit decimal, e.g. "3.00"
balanceFormatted: string; // display string, e.g. "$3.00"
currency: string;
}
interface MageTierReward {
id: string;
name: string;
discountType: string | null;
discountAmount: number | null;
pointsCost: number;
isActive: boolean;
isEntryReward?: boolean;
expiryDays?: number | null;
}
interface MageVipTier {
id: string;
name: string;
description: string | null;
threshold: number;
thresholdPoints?: number | null;
thresholdSpend?: number | null;
thresholdOrders?: number | null;
pointsMultiplier: number;
badgeImageUrl: string | null;
badgeColor: string | null;
rewards: MageTierReward[];
}
interface MageCustomerEarningEntry {
id: string;
earningRuleId: string;
status: string;
points: number;
refundedPoints: number;
createdAt: string;
action: string | null;
name: string;
pointsExpiryAt: string | null;
earningRule: { id: string; name: string } | null;
}
interface MageReferralEarning {
id: string;
type: string;
status: string;
friendEmail: string;
createdAt: string;
friendDiscountCreatedAt?: string | null;
referrerDiscountCreatedAt?: string | null;
}
interface MageCustomerDetails {
id: string;
shopifyCustomerId: string;
email: string;
firstName: string;
lastName: string;
points: number;
lifetimePoints: number;
redeemedPoints: number;
isExcluded: boolean;
dateOfBirth: string | null;
referralCode: string | null;
referralUrl: string | null;
tier: MageVipTier | null;
tierProgressValue: number;
tierProgressValueExtra?: number | null;
customerEarningRule: MageCustomerEarningEntry[];
earningHistory: MageCustomerEarningEntry[]; // back-compat alias
referralEarnings: MageReferralEarning[];
storeCredit?: MageStoreCreditBalance | null;
createdAt: string;
updatedAt: string;
}
interface MageFlexibleConfig {
conversionRate: number;
perPoints: number;
minPoints: number;
maxPoints: number;
}
interface MageReward {
id: string;
name: string;
description: string;
descriptionRequirements?: { type: string; text: string }[];
pointsCost: number;
category: string;
discountType: string | null;
discountAmount?: number | null;
iconUrl?: string | null;
tier: MageVipTier | null;
minimumSpend?: string;
redemptionMode?: 'fixed' | 'flexible';
purchaseType?: 'ONE_TIME' | 'SUBSCRIPTION' | 'BOTH';
recurringCycleLimit?: number | null;
flexibleConfig?: MageFlexibleConfig | null;
productMetadata?: {
productId?: string;
productTitle?: string;
productImage?: string | null;
productImageAlt?: string | null;
productHandle?: string | null;
items?: Array<{
id: string;
title: string;
image: string | null;
handle?: string;
type: 'product' | 'collection';
}>;
catalogType?: 'product' | 'collection';
} | null;
}
interface MageCustomerReward {
id: string;
rewardName: string;
rewardDescription?: string;
points: number;
discountCode: string;
discountType?: string | null;
discountValue?: string | null;
status: string;
createdAt: string;
expiresAt: string | null;
discountCodeRedeemedAt?: string | null;
productImage?: string | null;
validVariants?: unknown[];
isCustomerReward?: boolean;
isReferral?: boolean;
isReferrer?: boolean;
}
interface MageEarningRule {
id: string;
name: string;
description: string | null;
action: string;
category: string;
isActive: boolean;
pointsToGive: number;
customerFacingLabel: string | null;
callToActionUrl: string | null;
fullCallToActionUrl?: string | null;
points?: number; // final award after boosts (display this)
basePoints?: number;
tierBoost?: number | null;
campaignBoost?: number | null;
photoReviewPoints?: number | null;
videoReviewPoints?: number | null;
receiptConfig?: Record | null;
reviewConfig?: Record | null;
photoChallengeConfig?: Record | null;
customerEarningRule?: {
id: string;
earningRuleId: string;
status: string;
points: number;
createdAt: string;
} | null;
}
type MageActivityEntryType = 'earning' | 'redemption' | 'cashback';
interface MageActivityEntry {
id: string;
type: MageActivityEntryType;
name: string;
points: number;
refundedPoints: number;
status: string;
createdAt: string;
pointsExpiryAt: string | null;
amount?: number; // cashback rows only
refundedAmount?: number; // cashback rows only
currencySymbol?: string; // cashback rows only
}
interface MageCustomerProfile {
firstName: string;
lastName: string;
email?: string;
acceptsMarketing?: boolean;
}
interface MageReferralStats {
summary: {
totalReferrals: number;
pending: number;
friendConverted: number;
completed: number;
expired: number;
availableRewards: number;
};
referrals: Array<{
id: string;
friendEmail: string;
status: string;
createdAt: string;
friendOrderDate?: string | null;
referrerDiscountCode?: string | null;
referrerDiscountUsed?: boolean;
referrerDiscountExpiresAt?: string | null;
}>;
}
interface MageReferralLink {
enabled: boolean;
referralCode?: string;
referralUrl?: string;
stats?: {
totalReferred: number;
completedReferrals: number;
pendingReferrals: number;
};
socialSharing?: {
twitter: boolean;
twitterMessage: string | null;
facebook: boolean;
email: boolean;
whatsapp: boolean;
whatsappMessage: string | null;
sms?: boolean;
smsMessage?: string | null;
};
message?: string;
}
type MageProductBonusCampaign =
| {
type: 'multiplier';
name: string;
multiplier: number;
endsAt: string; // UTC ISO 8601
requiredProducts: { met: boolean; productIds: string[] } | null;
}
| {
type: 'tiered';
name: string;
tiers: { spendThreshold: number; spendFormatted: string; bonusValue: number; bonusFormatted: string }[];
endsAt: string; // UTC ISO 8601
requiredProducts: { met: boolean; productIds: string[] } | null;
};
interface MageLoyaltySDK {
// Customers
getCustomerDetails(): Promise>;
getCustomerVipTier(): Promise>;
updateCustomerDob(dob: string): Promise;
getProfile(): Promise>;
updateProfile(input: { firstName: string; lastName: string; acceptsMarketing?: boolean }): Promise>;
getCustomerActivity(options?: { limit?: number; offset?: number }): Promise>;
// Rewards
getShopRewards(): Promise>;
getCustomerRewards(): Promise>;
redeemReward(rewardId: string): Promise>;
redeemFlexible(rewardId: string, pointsToRedeem: number): Promise>;
// Cart
getCartEarningValue(): Promise>;
// Product page
getProductEarningValue(options: { productId: number | string; variantId?: number | string; price?: number; quantity?: number }): Promise>;
getProductBonusCampaign(options: { productId: number | string }): Promise>;
// VIP Tiers
getShopVipTiers(): Promise>;
// Earning Rules
getEarningRules(): Promise>;
// Referrals
getCustomerReferralStats(): Promise>;
getReferralLink(): Promise>;
// Sidebar
openSidebar(): void;
closeSidebar(): void;
// Loyalty value formatting (points vs store credit)
formatLoyaltyValue(value: number, mode?: string | null, currency?: string | null): string;
formatSignedLoyaltyValue(value: number, mode?: string | null, currency?: string | null): string;
loyaltyUnitLabel(mode?: string | null, count?: number): string;
resolveLoyaltyMode(mode?: string | null): MageLoyaltyMode;
isStoreCredit(mode?: string | null): boolean;
minorToMajor(amountMinor: number, currency?: string | null): number;
}
interface Window {
MageSDK: MageLoyaltySDK;
}
```
Make sure the file is included in your `tsconfig.json` (it will be automatically if it's inside your project root or `include` paths).
## Usage
Once the declaration file is in place, you get full autocomplete and type checking:
```typescript theme={null}
// TypeScript now knows about window.MageSDK
const resp = await window.MageSDK.getCustomerDetails();
if (resp.success && resp.data) {
const { customer, loyaltyMode, currency } = resp.data;
// Format the balance correctly whether the shop runs points or store credit
const label = window.MageSDK.formatLoyaltyValue(customer.points, loyaltyMode, currency);
console.log(label); // "450" (points) or "$4.50" (store credit)
console.log(customer.tier?.name); // string | undefined
}
```
# getShopVipTiers
Source: https://developers.mageloyalty.com/js-sdk/vip-tiers/get-shop-vip-tiers
List all VIP tiers configured for the shop.
Retrieve all VIP tiers for the shop, including their requirements and point multipliers.
## Usage
```javascript theme={null}
MageSDK.getShopVipTiers().then(function(resp) {
if (resp.success) {
resp.data.tiers.forEach(function(tier) {
console.log(tier.name, tier.threshold);
});
}
});
```
## Response
Whether the request was successful.
List of VIP tiers ordered by threshold.
The tier ID.
Display name of the tier.
Description of the tier, or `null` if not set.
The tier ranking value, mirroring the requirement for the shop's primary entry method.
Points requirement, set when points is a primary or extra entry criterion.
Spend requirement, set when spend is a primary or extra entry criterion.
Orders requirement, set when orders is a primary or extra entry criterion.
Points earning multiplier for this tier.
URL of the tier badge image, or `null` if not set.
Hex color code for the tier badge, or `null` if not set.
Active rewards associated with this tier.
The reward ID.
Display name of the reward.
Type of discount applied.
Cost to redeem, in the shop's loyalty unit.
The discount amount.
Whether the reward is currently active.
Whether this reward is granted automatically on reaching the tier.
Number of days before the reward expires, or `null` if it does not expire.
How customers qualify for tiers (e.g. `"points"`, `"spend"`, `"orders"`).
A second entry method when the shop combines two criteria, or `null`.
`"AND"` or `"OR"`, describing how `entryMethod` and `entryMethodExtra` combine.
The shop's currency symbol.
The multiplier mode, or `null` if not configured.
`"points"` or `"store_credit"`. See [Loyalty modes](/js-sdk/loyalty-modes).
The shop's ISO currency code, or `null`.
## Sample response
```json theme={null}
{
"success": true,
"data": {
"tiers": [
{
"id": "tier_bronze",
"name": "Bronze",
"description": "Welcome to the loyalty program!",
"threshold": 0,
"thresholdPoints": 0,
"thresholdSpend": null,
"thresholdOrders": null,
"pointsMultiplier": 1.0,
"badgeImageUrl": null,
"badgeColor": "#CD7F32",
"rewards": []
},
{
"id": "tier_gold",
"name": "Gold",
"description": "Our most valued customers",
"threshold": 500,
"thresholdPoints": 500,
"thresholdSpend": null,
"thresholdOrders": null,
"pointsMultiplier": 1.5,
"badgeImageUrl": null,
"badgeColor": "#FFD700",
"rewards": [
{
"id": "reward_gold_1",
"name": "Free shipping",
"discountType": "free_shipping",
"pointsCost": 200,
"discountAmount": null,
"isActive": true,
"isEntryReward": false,
"expiryDays": null
}
]
}
],
"entryMethod": "points",
"entryMethodExtra": null,
"entryLogic": "AND",
"currencySymbol": "£",
"pointsMultiplierMode": null,
"loyaltyMode": "points",
"currency": "GBP"
}
}
```