REST API v2
Overview
The Joy Loyalty Program REST API v2 provides comprehensive access to loyalty program functionality including customer management, point transactions, rewards, VIP tiers, and referral systems. This RESTful API follows consistent patterns and includes advanced features like cursor-based pagination, comprehensive error handling, and automatic data filtering.
API Base URLs
Production:
https://api.joy.so
Development:
https://dev-api.joy.so
Plan Requirements
All endpoints require Advanced or Enterprise plans.
Authentication
The API uses header-based authentication with two required headers for all requests:
X-Joy-Loyalty-App-Key: your_app_key_here
X-Joy-Loyalty-Secret-Key: your_secret_key_here
You can retrieve these credentials from your Joy Loyalty app settings page in the Shopify admin.
Authentication Example
curl -X GET "https://api.joy.so/rest_api/v2/customers" \
-H "X-Joy-Loyalty-App-Key: your_app_key" \
-H "X-Joy-Loyalty-Secret-Key: your_secret_key" \
-H "Content-Type: application/json"
Response Format
All API responses follow a consistent envelope structure:
Success Response
{
"success": true,
"data": {}, // or []
"meta": {
"count": 25, // for list responses
"pagination": { // for paginated responses
"hasNext": true,
"hasPre": false,
"total": 1250, // when hasCount=true
"totalPage": 63 // when hasCount=true
}
},
"timestamp": "2023-07-28T07:27:54.123Z",
"message": "Operation completed successfully" // optional
}
Error Response
{
"success": false,
"error": {
"message": "Resource not found",
"code": "NOT_FOUND",
"statusCode": 404,
"details": {} // optional
},
"timestamp": "2023-07-28T07:27:54.123Z"
}
Data Filtering
API responses only include fields with actual data. Fields with null
or undefined
values are automatically filtered out to provide cleaner responses for customer-facing applications.
Pagination
The API uses cursor-based pagination for efficient navigation through large datasets:
Parameters
before
: Firestore document ID to paginate before (previous page)after
: Firestore document ID to paginate after (next page)limit
: Page size (endpoint-specific defaults, max: 1000)hasCount
: Include total counts (may increase response time)
Default Limits
Customers: 20 items per page
Activities: 10 items per page
Rewards: 10 items per page
Navigation
Use the document ID from:
First item with
before
parameter for previous pageLast item with
after
parameter for next page
Example
# First page
curl "https://api.joy.so/rest_api/v2/customers?limit=20&hasCount=true"
# Next page (using last item's ID from previous response)
curl "https://api.joy.so/rest_api/v2/customers?after=abc123&limit=20"
# Previous page (using first item's ID)
curl "https://api.joy.so/rest_api/v2/customers?before=abc123&limit=20"
Common Query Parameters
Most list endpoints support these filtering and sorting parameters:
Date Filtering
created_at_min
: Filter records created after this datecreated_at_max
: Filter records created before this dateupdated_at_min
: Filter records updated after this dateupdated_at_max
: Filter records updated before this date
Sorting
order
: Sort order (varies by endpoint)Common values:
createdAt_desc
,createdAt_asc
,updatedAt_desc
,updatedAt_asc
Example with Filters
curl "https://api.joy.so/rest_api/v2/customers?\
created_at_min=2023-01-01T00:00:00Z&\
created_at_max=2023-12-31T23:59:59Z&\
order=createdAt_desc&\
limit=50"
Error Handling
HTTP Status Codes
200
Success
Request completed successfully
400
Bad Request
Invalid parameters or request data
401
Unauthorized
Missing or invalid authentication
403
Forbidden
Plan upgrade required or shop uninstalled
404
Not Found
Resource doesn't exist
500
Internal Server Error
Unexpected server error
Common Error Codes
CUSTOMER_NOT_FOUND
Customer doesn't exist
Verify customer ID
PROGRAM_NOT_FOUND
Program doesn't exist
Check program ID
TIER_NOT_FOUND
Tier doesn't exist
Verify tier ID
PLAN_UPGRADE_REQUIRED
Feature requires higher plan
Upgrade subscription
SHOP_UNINSTALLED
App not installed on shop
Reinstall the app
Error Response Example
{
"success": false,
"error": {
"message": "Customer not found",
"code": "CUSTOMER_NOT_FOUND",
"statusCode": 404
},
"timestamp": "2023-07-28T07:27:54.123Z"
}
Rate Limiting
The API implements rate limiting to ensure fair usage:
Default limit: 1000 requests per hour per shop
Headers included in response:
X-RateLimit-Limit
: Maximum requests allowedX-RateLimit-Remaining
: Requests remaining in windowX-RateLimit-Reset
: Time when limit resets (Unix timestamp)
When rate limited, you'll receive a 429 Too Many Requests
response.
Best Practices
1. Efficient Pagination
// Use hasCount sparingly to avoid performance impact
const customers = await fetch('/rest_api/v2/customers?hasCount=false');
// Navigate using document IDs, not page numbers
const nextPage = await fetch(`/rest_api/v2/customers?after=${lastCustomerId}`);
2. Date Range Queries
// Use proper ISO 8601 format for dates
const recentCustomers = await fetch(
'/rest_api/v2/customers?created_at_min=2023-07-01T00:00:00Z'
);
3. Error Handling
async function handleApiCall(url) {
const response = await fetch(url);
const data = await response.json();
if (!data.success) {
throw new Error(`${data.error.code}: ${data.error.message}`);
}
return data.data;
}
4. Authentication Security
Store credentials securely (environment variables)
Never expose credentials in client-side code
Rotate credentials regularly
Monitor for unauthorized usage
5. Batch Operations
// Instead of multiple single requests
customers.forEach(id => updateCustomer(id)); // ❌ Inefficient
// Use bulk operations when available
await updateMultipleCustomers(customerIds); // ✅ Efficient
Data Types and Formats
Date Formats
ISO 8601:
2023-07-28T07:27:54.123Z
(API responses)Date Only:
YYYY-MM-DD
for date fieldsBirthday:
MM/DD
format for birthday fields
Customer Types
member
: Active loyalty program memberguest
: Guest customer (not joined program)left
: Former member who left program
Activity Types
earn_point
: Points earnedredeem_point
: Points spent/redeemedadjust_point
: Points adjusted by admin
Activity Sources
admin
: Admin panel actionuser
: Customer actionrest_api
: API actionwebhook
: Webhook trigger
Program Types
earning
: Point earning programspending
: Point spending/redemption programtier_spending
: Tier-specific spending programtier
: Tier configuration program
Reward Status
active
: Available for useused
: Already redeemedexpired
: Past expiration date
Security Considerations
1. Data Privacy
Customer emails and personal data are included in responses
Ensure compliance with privacy regulations (GDPR, CCPA)
Implement proper access controls in your application
2. API Key Security
Use HTTPS only for all API requests
Store API keys securely (never in version control)
Implement key rotation policies
Monitor for suspicious activity
3. Input Validation
Validate all input data on your end
Use proper data types for API calls
Sanitize user input before sending to API
4. Error Information
Error responses may contain sensitive information
Log errors securely without exposing to end users
Implement proper error boundaries
Integration Examples
JavaScript/Node.js
class JoyApiClient {
constructor(appKey, secretKey, baseUrl = 'https://api.joy.so') {
this.appKey = appKey;
this.secretKey = secretKey;
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Joy-Loyalty-App-Key': this.appKey,
'X-Joy-Loyalty-Secret-Key': this.secretKey,
...options.headers
}
});
const data = await response.json();
if (!data.success) {
throw new Error(`${data.error.code}: ${data.error.message}`);
}
return data;
}
async getCustomers(params = {}) {
const queryString = new URLSearchParams(params).toString();
return this.request(`/rest_api/v2/customers?${queryString}`);
}
async awardPoints(shopifyCustomerId, points, note) {
return this.request('/rest_api/v2/transactions/points/award', {
method: 'POST',
body: JSON.stringify({
shopifyCustomerId,
point: points,
adminNote: note
})
});
}
}
// Usage
const client = new JoyApiClient('your-app-key', 'your-secret-key');
const customers = await client.getCustomers({ limit: 20 });
Python
import requests
from typing import Dict, Optional
class JoyApiClient:
def __init__(self, app_key: str, secret_key: str, base_url: str = 'https://api.joy.so'):
self.app_key = app_key
self.secret_key = secret_key
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json',
'X-Joy-Loyalty-App-Key': app_key,
'X-Joy-Loyalty-Secret-Key': secret_key
})
def request(self, endpoint: str, method: str = 'GET', data: Optional[Dict] = None):
url = f"{self.base_url}{endpoint}"
response = self.session.request(method, url, json=data)
result = response.json()
if not result.get('success'):
error = result.get('error', {})
raise Exception(f"{error.get('code', 'UNKNOWN_ERROR')}: {error.get('message', 'Unknown error')}")
return result
def get_customers(self, **params):
query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
endpoint = f"/rest_api/v2/customers?{query_string}"
return self.request(endpoint)
def award_points(self, shopify_customer_id: str, points: int, note: str = ''):
return self.request('/rest_api/v2/transactions/points/award', 'POST', {
'shopifyCustomerId': shopify_customer_id,
'point': points,
'adminNote': note
})
# Usage
client = JoyApiClient('your-app-key', 'your-secret-key')
customers = client.get_customers(limit=20, order='createdAt_desc')
PHP
<?php
class JoyApiClient {
private $appKey;
private $secretKey;
private $baseUrl;
public function __construct($appKey, $secretKey, $baseUrl = 'https://api.joy.so') {
$this->appKey = $appKey;
$this->secretKey = $secretKey;
$this->baseUrl = $baseUrl;
}
public function request($endpoint, $method = 'GET', $data = null) {
$url = $this->baseUrl . $endpoint;
$headers = [
'Content-Type: application/json',
'X-Joy-Loyalty-App-Key: ' . $this->appKey,
'X-Joy-Loyalty-Secret-Key: ' . $this->secretKey
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $data ? json_encode($data) : null
]);
$response = curl_exec($curl);
curl_close($curl);
$result = json_decode($response, true);
if (!$result['success']) {
$error = $result['error'] ?? [];
throw new Exception(($error['code'] ?? 'UNKNOWN_ERROR') . ': ' . ($error['message'] ?? 'Unknown error'));
}
return $result;
}
public function getCustomers($params = []) {
$queryString = http_build_query($params);
return $this->request("/rest_api/v2/customers?$queryString");
}
public function awardPoints($shopifyCustomerId, $points, $note = '') {
return $this->request('/rest_api/v2/transactions/points/award', 'POST', [
'shopifyCustomerId' => $shopifyCustomerId,
'point' => $points,
'adminNote' => $note
]);
}
}
// Usage
$client = new JoyApiClient('your-app-key', 'your-secret-key');
$customers = $client->getCustomers(['limit' => 20]);
?>
Changelog
Version 2.0.0 (Current)
Initial release of REST API v2
Cursor-based pagination implementation
Consistent response envelope format
Comprehensive error handling
Automatic data filtering
Advanced plan requirement enforcement
Support
For technical support and questions:
Documentation: This guide covers most use cases
API Status: Check API status at status.joy.so
Support Portal: Contact through Joy Loyalty support
Community: Join the Joy Loyalty developer community
Next Steps
Get Started: Set up authentication and make your first API call
Explore Endpoints: Review the detailed endpoint documentation below
Test Integration: Use the provided code examples
Production Deployment: Implement proper error handling and security
This documentation covers the core concepts and best practices for the Joy Loyalty REST API v2. For detailed endpoint specifications, continue reading the endpoint-specific documentation sections.