Introduction
Welcome to the Casa Loyalty External API. This API allows you to integrate loyalty program functionality into your external platforms, mobile apps, POS systems, and custom solutions.
https://app.casaloyalty.io/api/external
Key Features
- Register and retrieve customers
- Process orders and award loyalty points
- Manually credit or debit customer points
- Retrieve customer order history and activity
- Access earning rules and reward catalogs
- View VIP tiers and perks
- List and inspect referrals
- Create and complete referrals via API
- Create redemptions on behalf of customers
- Submit and list earning activities
- Update customer profile fields
- Force VIP tier assignments
- Retrieve program settings
- Manage translation locales
- Retrieve individual activities by ID
Authentication
Send the key in Authorization. Bearer form is recommended; the current middleware also accepts the raw key as the whole header value.
Authorization: Bearer cls_live_your_api_keyX-API-Key is not read. Keys are tenant-bound, and current routes request the generic read or write permission displayed below.
Getting Your API Key
- Log in to your CasaLoyalty admin dashboard
- Navigate to Settings → API Keys
- Click Generate API Key and select the desired resource permissions
- Copy and securely store your API key — it is shown only once and cannot be retrieved later
API-key lifecycle
- The supported Admin flow creates
cls_live_keys. A shop can create multiple keys, choose a name, select the displayed resource permissions, and set the per-key hourly request count. - The complete plaintext key is displayed only after creation. Casa stores a secure hash and non-secret
cls_live_prefix; the plaintext cannot be revealed again. - Admin lists the name, masked prefix, stored permissions, configured hourly count, last-used date, and active/revoked status. Revocation makes a key inactive.
- There is no supported reactivation, deletion, or in-place rotation operation. Rotate manually by creating a replacement, updating the integration, and then revoking the old key.
- The supported Admin UI does not configure expiration or IP allowlists and exposes no post-creation edit flow. Backend fields and a dormant update action are not a supported self-service contract.
- The validated key supplies the shop tenant. Each route checks a specific
resource:actionpermission (e.g.,customers:read,rewards:write).
Resource-level permissions
Each endpoint requires a specific resource permission. The Admin UI lets merchants configure per-resource access when creating API keys.
Permission format: resource:action (e.g., customers:read, rewards:write)
Available permissions
| Resource | Read | Write | Endpoints |
|---|---|---|---|
customers | ✅ | ✅ | List/get/register customers, update customer, force VIP tier |
orders | ✅ | ✅ | Customer orders history, process order |
activities | ✅ | ✅ | List activities, get by ID, submit activity |
earning_rules | ✅ | — | List earning rules |
rewards | ✅ | ✅ | List/get/create/update/delete rewards |
redemptions | ✅ | ✅ | List/create redemptions |
referrals | ✅ | ✅ | List/get/create/complete/block referrals, advocate stats |
points | — | ✅ | Update points, batch update points |
vip_tiers | ✅ | — | List/get VIP tiers |
settings | ✅ | — | Get program settings |
translations | ✅ | ✅ | List/get/create/update/delete translations |
Backward compatibility: API keys created before granular permissions only have generic read/write. These keys continue to work — when a granular permission isn’t explicitly set, the system falls back to the generic permission. Only keys with an explicit false for a specific resource are denied.
Permission dependencies: Write operations that trigger side effects (e.g., processing an order credits points, completing a referral issues rewards) do NOT require separate permissions for the side effect. The permission gates the primary action; business-logic side effects are part of that action.
Response and error formats
Successful endpoint fields are top-level; there is no shared data wrapper.
{
"success": true,
"customer": {}
}Authentication middleware errors use error.details:
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid, expired, or has been revoked",
"details": {}
}
}Business errors put operation-specific fields directly inside error. Validation failures are HTTP 400 and include error.validationErrors[]. Unsupported methods return HTTP 405 with { "error": "Method not allowed" }, outside the standard envelope.
| HTTP | Codes |
|---|---|
| 400 | VALIDATION_ERROR, SHOPIFY_VALIDATION_ERROR |
| 401 | MISSING_API_KEY, INVALID_API_KEY |
| 403 | IP_NOT_WHITELISTED (legacy/operator-configured allowlist; not self-service), INSUFFICIENT_PERMISSIONS, CUSTOMER_BLOCKED |
| 404 | CUSTOMER_NOT_FOUND |
| 422 | DUPLICATE_ORDER, EARNING_LIMIT_REACHED, INSUFFICIENT_POINTS |
| 429 | RATE_LIMIT_EXCEEDED |
| 503 | ACCOUNTING_TEMPORARILY_UNAVAILABLE; response includes Retry-After: 5 |
| 500 | INTERNAL_ERROR |
Error Codes
| Code | HTTP | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Request validation failed (invalid or missing parameters) |
MISSING_API_KEY | 401 | No Authorization header supplied |
INVALID_API_KEY | 401 | Key is unknown, inactive, expired, or invalid |
IP_NOT_WHITELISTED | 403 | Operator-configured allowlist rejected the IP |
INSUFFICIENT_PERMISSIONS | 403 | Key lacks the required permission |
CUSTOMER_BLOCKED | 403 | Customer account is blocked |
CUSTOMER_NOT_FOUND | 404 | No enabled customer matched the identifiers |
EARNING_RULE_NOT_FOUND | 404 | No active earning rule with that ID |
REWARD_NOT_FOUND | 404 | No active reward with that ID |
VIP_TIER_NOT_FOUND | 404 | No VIP tier with that ID |
REFERRAL_NOT_FOUND | 404 | No referral with that ID in this shop |
DUPLICATE_ORDER | 422 | External order ID already processed |
EARNING_LIMIT_REACHED | 422 | Customer reached the earning limit for this rule |
INSUFFICIENT_POINTS | 422 | Requested debit exceeds current balance |
RATE_LIMIT_EXCEEDED | 429 | Per-key hourly request limit exceeded |
ACCOUNTING_TEMPORARILY_UNAVAILABLE | 503 | Points accounting is temporarily unavailable; retry after Retry-After header |
INTERNAL_ERROR | 500 | Unhandled server error |
Rate limiting
Each API key has a per-key hourly request limit, configured by the merchant during Admin key creation. HTTP 429 returns RATE_LIMIT_EXCEEDED and error.details.rateLimit.
Pagination
All collection endpoints support offset pagination, and the six endpoints listed below additionally support cursor-based pagination via since_id.
Offset pagination (default)
limit is 1–100 (default 50), and offset is non-negative (default 0). Pagination responses contain total, limit, offset, hasMore, currentPage, and totalPages.
Cursor-based pagination (since_id)
Pass an optional since_id query parameter to switch to cursor mode. The response returns items with id > since_id, ordered by id ASC. When since_id is present, offset is ignored and currentPage/totalPages are null.
Supported endpoints
GET /customersGET /customer/ordersGET /customer/activityGET /activitiesGET /referralsGET /redemptions
Response pagination object in cursor mode
{
"pagination": {
"total": 1250,
"limit": 20,
"offset": 0,
"hasMore": true,
"currentPage": null,
"totalPages": null,
"nextCursor": 520
}
}nextCursor is the last item's ID in the current page (use it as since_id for the next request), or null when there are no more pages.
Example
GET /api/external/customers?limit=20&since_id=500
Authorization: Bearer cls_live_your_api_keyTo fetch the next page, use the returned nextCursor:
GET /api/external/customers?limit=20&since_id=520When to use each mode
- Offset — jumping to a specific page number, showing page controls in a UI.
- Cursor — iterating through large datasets, real-time syncs, avoiding skipped/duplicate items when data changes between pages.
since_id preserves the existing offset behavior.The earning-rules and rewards catalog endpoints do not paginate; they return all active rows in a single response.
Locale and point-label tokens
All published operations accept an optional locale query parameter. When absent, Casa uses the shop's content/primary language, with an English fallback. The locale selects the singular/plural point labels used to resolve Casa point-label tokens in response strings.
This is token substitution, not blanket translation. Merchant-authored names, activity descriptions, metadata, and custom strings are not guaranteed to be translated.
Idempotency and retries
There is no general Idempotency-Key header contract. Repeating a successful write request repeats the mutation. Do not automatically retry after a timeout or ambiguous transport failure unless you independently verify whether the first mutation committed.
Endpoints with built-in duplicate protection
- Register customer looks up an existing enabled customer by email and then phone.
- Process order rejects a duplicate
(shopId, externalOrderId)with HTTP 422. - Submit activity (POST /activities) with
externalReferenceIdrejects duplicates with the original result.
Published endpoints
Customer identifier objects accept at least one of positive integer Casa id, Shopify customerId (string or number in request bodies), valid email, or international phone. Phone normalization removes spaces, dashes, and parentheses and adds a leading +; encode a literal plus as %2B in URLs. Lookup precedence is id, email, phone, then Shopify ID, always within the authenticated key's shop.
Disabled customers are excluded and normally return CUSTOMER_NOT_FOUND. Blocked customers remain readable but mutations return CUSTOMER_BLOCKED. If enabled rows share a phone, lookup returns the most recently updated match.
Current customer responses expose id, string customerId, firstName, lastName, email, phone, loyaltyPoints, loyaltyStatus, vipPoints, spendings, status, dateOfBirth, anniversaryDate, createdAt, updatedAt, isBlocked, isDisabled, nullable vipTier, vipTierExpiresAt, and nullable referral. VIP tier contains id, name, minPointsThreshold, and pointsMultiplier; referral contains referralUrl and referralCount. This current API has no normalized customer-identity resource.
The loyaltyStatus field is a derived convenience field: member (active, not blocked), blocked (blocked from earning/redeeming), or disabled (excluded from program).
Supports the shared optional locale query parameter.
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | Yes | 1–100 characters. Example: "Ada" |
lastName | string | Yes | 1–100 characters. Example: "Lovelace" |
email | string | Conditional | Valid email. Required if phone not provided. Example: "ada@example.com" |
phone | string | Conditional | International phone number. Required if email not provided. Example: "+16143752923" |
dateOfBirth | string | No | Date string, no format enforced. Example: "1990-05-15" |
initialPoints | integer | No | Non-negative. Default: 0. Points credited to new customers only (ignored for existing). Example: 100 |
metadata | object | No | Arbitrary JSON. Stored on creation only, not returned by any read endpoint. Example: {"source": "mobile_app"} |
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+16143752923",
"dateOfBirth": "1990-05-15",
"initialPoints": 100,
"metadata": {
"source": "mobile_app",
"campaign": "summer2026"
}
}Success response
{
"success": true,
"status": "created",
"customer": {
"id": 42,
"customerId": "8986033979714",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": null,
"loyaltyPoints": 100,
"loyaltyStatus": "member",
"vipPoints": 0,
"spendings": 0,
"status": "MEMBER",
"dateOfBirth": null,
"anniversaryDate": null,
"createdAt": "2026-01-02T10:00:00.000Z",
"updatedAt": "2026-01-02T10:00:00.000Z",
"isBlocked": false,
"isDisabled": false,
"vipTier": null,
"vipTierExpiresAt": null,
"referral": null
}
}Returns HTTP 201 with top-level customer and status: "created", or HTTP 200 with status: "existing" and message. It can also award configured signup rewards. If the shop has a signup-bonus earning rule configured, the reward is awarded automatically. VIP tier evaluation is triggered after the signup bonus is applied. Errors include 400 validation/Shopify validation, 403 blocked, 503 temporarily unavailable, and shared errors.
Supports the shared optional locale query parameter.
GET /api/external/customer?email=john@example.com
Authorization: Bearer cls_live_your_api_keySuccess response — HTTP 200
{
"success": true,
"customer": {
"id": 42,
"customerId": "8986033979714",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+16143752923",
"loyaltyPoints": 450,
"loyaltyStatus": "member",
"vipPoints": 850,
"spendings": 1250.5,
"status": "MEMBER",
"dateOfBirth": "1990-05-15T00:00:00.000Z",
"anniversaryDate": "2024-06-20",
"createdAt": "2026-01-01T10:30:00.000Z",
"updatedAt": "2026-08-15T09:22:00.000Z",
"isBlocked": false,
"isDisabled": false,
"vipTier": {
"id": 2,
"name": "Gold",
"minPointsThreshold": 500,
"pointsMultiplier": 1.5
},
"vipTierExpiresAt": "2027-01-01T00:00:00.000Z",
"nextVipTier": {
"id": 3,
"name": "Platinum",
"minPointsThreshold": 1000
},
"deltaToNextTier": 150,
"referral": {
"referralUrl": "abc123XYZ",
"referralCount": 5
},
"lifetimeEarnings": 2350,
"orderCount": 12,
"referralStats": {
"completed": 3,
"pending": 1,
"total": 4
}
}
}Customer fields include internal id, string Shopify customerId, profile, points/spend/status, loyaltyStatus, DOB, anniversaryDate, createdAt, updatedAt, block/disable flags, optional VIP tier, vipTierExpiresAt, nextVipTier, deltaToNextTier, and referral. Detail endpoints (GET /customer, GET /customers/:id) also include lifetimeEarnings, orderCount, and referralStats; these are null on list endpoints. VIP tier fields are id, name, minPointsThreshold, and pointsMultiplier.
Accepts any current customer identifier. source is shopify, external_api, fusion_platform, pos, or all (default all). Optional ISO startDate/endDate filter createdAt inclusively; limit is 1–100 (default 50), offset is non-negative (default 0), and locale is optional.
Results are newest first. After customer lookup, order history is associated through the customer's Shopify numeric customerId, not Casa internal id. Returns HTTP 200 top-level orders and pagination; there is no top-level customer.
{
"success": true,
"orders": [{
"id": 1001,
"orderId": "12345",
"externalOrderId": "ext_order_123",
"orderName": "#1001",
"orderValue": 150,
"currency": "USD",
"source": "shopify",
"metadata": {},
"status": "active",
"pointsEarned": {
"regular": 150,
"incentive": 0,
"total": 150
},
"spending": {
"eligible": 150,
"vipQualifying": 0
},
"placedAt": null,
"createdAt": "2026-01-02T14:30:00.000Z",
"updatedAt": "2026-01-02T14:30:00.000Z"
}],
"pagination": {
"total": 1,
"limit": 10,
"offset": 0,
"hasMore": false,
"currentPage": 1,
"totalPages": 1
}
}id is Casa's internal Order row ID. orderId is the stored/generated order identifier serialized as a string. externalOrderId is the external/provider-facing ID when present.
metadata may contain additional fields and is not a stable nested contract. Do not depend on undocumented nested keys. Errors include shared errors, HTTP 400 VALIDATION_ERROR, HTTP 404 CUSTOMER_NOT_FOUND, and HTTP 500 INTERNAL_ERROR.
Accepts any current customer identifier. Query activityType is points, redemption, order, referral, vip, or all (default all). Optional ISO startDate/endDate filter createdAt inclusively; source is an exact string; limit is 1–100 (default 50), offset is non-negative (default 0), and locale is optional.
Only non-cancelled rows are returned, newest first, as top-level activities and pagination; there is no top-level customer. activityType is the query filter, while type is the response field.
{
"success": true,
"activities": [{
"id": 5001,
"type": "points",
"description": "Points earned from order #1001",
"pointsChange": 15,
"pointsBalance": 465,
"reason": "Order purchase",
"source": "shopify",
"approved": true,
"manualVerify": false,
"metadata": {},
"createdAt": "2026-01-02T14:30:00.000Z"
}],
"pagination": {
"total": 1,
"limit": 20,
"offset": 0,
"hasMore": false,
"currentPage": 1,
"totalPages": 1
}
}Each activity contains id, type, description, pointsChange, pointsBalance, reason, source, approved, manualVerify, metadata, and createdAt.
pointsChange is the delta. Historical pointsBalance is a running balance that may not be uniformly canonical for every activity type. This is not a canonical PointsTransaction resource. Errors include shared errors, HTTP 400 VALIDATION_ERROR, HTTP 404 CUSTOMER_NOT_FOUND, and HTTP 500 INTERNAL_ERROR.
Supports the shared optional locale query parameter.
Requires a customer identifier and an order object.
Required order fields
| Field | Type | Description |
|---|---|---|
orderId | string | Unique external order ID (used for duplicate detection) |
subtotal | number | Non-negative. Order subtotal before tax/shipping |
total | number | Non-negative. Order total including tax/shipping |
currency | string | 3-letter ISO currency code (e.g. USD, EUR). Auto-uppercased |
items | array | At least one item. Each item: title (required string), price (required, positive number), quantity (required, positive integer). Optional: sku, productId, variantId, tags (string array for no-points exclusion) |
Optional order fields
| Field | Type | Default | Description |
|---|---|---|---|
orderName | string | #orderId | Display name (e.g. #1001) |
source | string | external_api | Order source. Set to pos for POS orders — this triggers the reward channel eligibility check. Any other value is treated as online |
isSubscription | boolean | false | When true, uses the earning rule’s subscription points rate if configured |
placedAt | string | Current time | ISO 8601 datetime with timezone offset. When the order was placed on the external platform |
metadata | object | — | Arbitrary JSON stored in the order record |
paymentGateway | string|string[] | — | Payment gateway(s) used. If any matches the merchant’s excluded list, the order is rejected |
taxes | number | — | Non-negative. Tax amount. When provided with shipping, the base order value is computed as subtotal + taxes + shipping per the merchant’s settings instead of using total |
shipping | number | — | Non-negative. Shipping amount. See taxes |
{
"customer": { "email": "john@example.com" },
"order": {
"orderId": "external_order_12345",
"subtotal": 100,
"total": 110,
"currency": "USD",
"items": [{ "title": "T-Shirt", "price": 100, "quantity": 1 }]
}
}Returns HTTP 201 with numeric pointsAwarded, pointsBreakdown { basePoints, vipBonus, vipApplied, campaignApplied, birthdayMultiplier }, customer, and order. The request order ID is returned as externalOrderId; the response orderId is Casa's generated numeric ID serialized as a string.
{
"success": true,
"pointsAwarded": 11,
"pointsBreakdown": {
"basePoints": 11,
"vipBonus": 0,
"vipApplied": false,
"campaignApplied": null,
"birthdayMultiplier": null
},
"customer": {
"id": 42,
"loyaltyPoints": 461,
"loyaltyStatus": "member"
},
"order": {
"id": 1003,
"orderId": "1770000000000000",
"externalOrderId": "external_order_12345",
"orderName": "#external_order_12345",
"orderValue": 110,
"currency": "USD",
"source": "external_api",
"metadata": {
"externalPlatform": "external_api",
"externalOrderId": "external_order_12345",
"customMetadata": {}
},
"createdAt": "2026-01-02T15:00:00.000Z"
}
}When a bonus campaign is active, campaignApplied includes { campaignName, multiplierValue }. When the customer's birthday multiplier window is active, birthdayMultiplier includes { multiplier, bonusPoints }. Both are null when inactive.
Points-only endpoint: This endpoint handles loyalty points earning only. Order points are awarded synchronously; points-type one-time incentives (first-order / time-window / first-subscription bonuses) and points-type spending bonuses are awarded asynchronously after the order commits (the immediate pointsAwarded reflects order points only). Rewards that need a Shopify Admin object (discount codes, gift cards, store credit, free shipping), discount code settlement, and referral completion are not processed here. Use the dedicated POST /referrals/:id/complete and POST /redemptions endpoints for those workflows. VIP tier re-evaluation is triggered asynchronously.
The response customer and raw metadata objects are abbreviated. The metadata sample is illustrative and may contain additional keys that are not a stable nested contract.
Duplicate order, earning-limit, and customer-tag-exclusion failures are HTTP 422. A duplicate error is { "code": "DUPLICATE_ORDER", "message": "This order has already been processed", "orderId": "..." }. A tag exclusion error is { "code": "CUSTOMER_EXCLUDED_BY_TAG", "message": "This customer is excluded from earning points by a configured tag" }. Additional eligibility gate errors (all HTTP 422): ORDER_CHANNEL_EXCLUDED (reward channel mismatch), PAYMENT_GATEWAY_EXCLUDED (payment gateway in exclusion list), MINIMUM_SPEND_NOT_MET (order value below merchant's minimum spend requirement), PROGRAM_DISABLED (both loyalty and spending bonus programs disabled). This is a create-only path; it does not expose update, refund, or cancellation.
Requires a customer identifier object, non-zero integer pointsChange, and a 1–500 character reason. metadata is an optional object; applyVipMultiplier is a boolean defaulting to false. This operation does not consume locale.
{
"customer": { "email": "john@example.com" },
"pointsChange": 100,
"reason": "Service recovery",
"applyVipMultiplier": false
}Positive requests can receive a VIP bonus only when requested and enabled; negative requests are never multiplied. The applied positive amount updates loyalty and VIP points.
The requested base appears as pointsBreakdown.baseChange. Top-level pointsChange and pointsBreakdown.totalChange are the amount actually applied; they can differ because of a VIP bonus or a concurrent debit adjustment. vipBonus reports the bonus, and customer.previousPoints reports the pre-mutation balance observed by the route.
Success response
{
"success": true,
"pointsChange": 100,
"pointsBreakdown": {
"baseChange": 100,
"vipBonus": 0,
"totalChange": 100
},
"customer": {
"id": 42,
"loyaltyPoints": 550,
"loyaltyStatus": "member",
"previousPoints": 450
}
}Errors include HTTP 400 validation, 403 CUSTOMER_BLOCKED, 404 CUSTOMER_NOT_FOUND, 422 INSUFFICIENT_POINTS, 503 ACCOUNTING_TEMPORARILY_UNAVAILABLE with Retry-After: 5, and 500 INTERNAL_ERROR. The accounting helper prevents a debit from taking the balance below zero.
Batch Update Points
Adjust points for multiple customers in a single request. Synchronous processing, max 20 items per call. Per-item results with partial success support.
| Field | Type | Required | Description |
|---|---|---|---|
adjustments | array | Yes | Array of adjustment objects (1–20 items) |
adjustments[].customer | object | Yes | Standard identifier: id, customerId, email, or phone |
adjustments[].pointsChange | integer | Yes | Non-zero integer (positive to credit, negative to debit) |
adjustments[].reason | string | Yes | Reason for the adjustment (1–500 chars) |
adjustments[].applyVipMultiplier | boolean | No | Apply VIP tier points multiplier (default: false) |
curl -X POST 'https://app.casaloyalty.io/api/external/update-points/batch' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"adjustments": [
{ "customer": { "email": "a@example.com" }, "pointsChange": 100, "reason": "Migration credit" },
{ "customer": { "id": 42 }, "pointsChange": -50, "reason": "Manual correction" }
]
}'Success response — HTTP 200
{
"success": true,
"summary": { "total": 2, "succeeded": 2, "failed": 0 },
"results": [
{ "index": 0, "success": true, "pointsChange": 100, "customer": { "id": 10, "loyaltyPoints": 600, "previousPoints": 500 } },
{ "index": 1, "success": true, "pointsChange": -50, "customer": { "id": 42, "loyaltyPoints": 350, "previousPoints": 400 } }
]
}HTTP is always 200 if the batch itself was processed. Individual failures are reported per-item with error codes: CUSTOMER_NOT_FOUND, CUSTOMER_BLOCKED, INSUFFICIENT_POINTS, INTERNAL_ERROR.
Errors (batch-level): 400 VALIDATION_ERROR (invalid body, over 20 items, zero pointsChange), 401/403/429 auth/permission/rate.
List Customers
Search, filter, and paginate through customers in your shop. Disabled customers are always excluded from results.
Optional query: locale.
Query parameters
| Parameter | Type | Description |
|---|---|---|
search | string | Optional. Substring match across first name, last name, email, and phone |
minPoints | integer | Optional. Minimum loyalty points (inclusive) |
maxPoints | integer | Optional. Maximum loyalty points (inclusive) |
vipTierId | integer | Optional. Filter to customers in this VIP tier |
status | string | GUEST, MEMBER, or all (default all) |
isBlocked | boolean | Optional. true/1 filters blocked; false/0 filters unblocked |
createdAfter | ISO datetime | Optional. Customers created on or after this date |
createdBefore | ISO datetime | Optional. Customers created on or before this date |
sortBy | string | loyaltyPoints, vipPoints, spendings, or createdAt (default createdAt) |
sortOrder | string | asc or desc (default desc) |
limit | integer | 1–100 (default 50) |
offset | integer | Non-negative (default 0) |
Example request
curl 'https://app.casaloyalty.io/api/external/customers?search=john&minPoints=100&limit=20' \
-H 'Authorization: Bearer cls_live_your_api_key'Success response — HTTP 200
{
"success": true,
"customers": [
{
"id": 42,
"customerId": "8986033979714",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+16143752923",
"loyaltyPoints": 450,
"loyaltyStatus": "member",
"vipPoints": 850,
"spendings": 1250.5,
"status": "MEMBER",
"dateOfBirth": "1990-05-15T00:00:00.000Z",
"anniversaryDate": "2024-06-20",
"createdAt": "2026-01-01T10:30:00.000Z",
"updatedAt": "2026-08-15T09:22:00.000Z",
"isBlocked": false,
"isDisabled": false,
"vipTier": {
"id": 2,
"name": "Gold"
},
"vipTierExpiresAt": "2027-01-01T00:00:00.000Z",
"referral": {
"referralUrl": "abc123XYZ",
"referralCount": 5
}
}
],
"pagination": {
"total": 1,
"limit": 20,
"offset": 0,
"hasMore": false,
"currentPage": 1,
"totalPages": 1
}
}The collection customer shape matches the detail endpoint except vipTier contains only id and name (no minPointsThreshold or pointsMultiplier).
Errors include shared authentication/authorization/rate errors, HTTP 400 VALIDATION_ERROR, and HTTP 500 INTERNAL_ERROR.
Get Earning Rules
Returns all active earning-rule definitions for your shop — the catalog of ways customers can earn points. There is no pagination; all active rules are returned in a single response, ordered by placement.
Optional query: locale, customerId (integer), email, or phone — when any customer identifier is provided, each rule includes an eligibility object with eligible boolean and reasons[]. Reason codes: customer_blocked, vip_tier_not_eligible, vip_tier_excluded, earning_limit_reached.
Example request
curl 'https://app.casaloyalty.io/api/external/earning-rules' \
-H 'Authorization: Bearer cls_live_your_api_key'Success response — HTTP 200
{
"success": true,
"earningRules": [
{
"id": 1,
"code": "order-purchase",
"name": "Points per dollar spent",
"description": "Earn 1 point for every dollar spent",
"type": "order-purchase",
"points": 1,
"pointsSubscription": null,
"earningCalculationType": "incremental",
"calculationIncrement": 1,
"thresholdValue": null,
"isSubscription": false,
"oneTimeIncentive": {
"enabled": false,
"points": 0,
"subscriptionPoints": 0
},
"earningLimit": {
"enabled": false,
"limit": null,
"period": null
},
"vipTierEligibility": {
"enabled": false,
"include": false,
"tiers": []
},
"eligibleOrderType": "both",
"manualVerify": false,
"iconUrl": "https://cdn.example.com/icons/order.png",
"customTitle": null,
"customSubtitle": null,
"customDescription": null,
"placement": 1,
"status": "active"
}
],
"total": 1
}Field reference
| Field | Description |
|---|---|
code | Rule type identifier (e.g. order-purchase, signup-bonus, birthday, daily-visit) |
type | Alias of code for backwards compatibility |
points | Points per qualifying action (fixed) or per increment (incremental) |
earningCalculationType | fixed (flat) or incremental (per-unit) |
calculationIncrement | Monetary/unit increment for incremental calculation |
oneTimeIncentive | Additional bonus on first qualifying action |
earningLimit | Per-period award limit |
vipTierEligibility | Tier restriction (include or exclude specific tiers) |
manualVerify | Whether earning requires manual approval |
signupReward | Present only for signup-bonus; indicates it acts as the registration reward |
This endpoint returns rule definitions only. It does not evaluate customer eligibility or earning limits.
Errors include shared authentication/authorization/rate errors and HTTP 500 INTERNAL_ERROR.
Get Rewards
Returns all active reward definitions (redemption options) for your shop. There is no pagination; all active rewards are returned in a single response, ordered by placement.
Optional query: locale.
Query parameters
| Parameter | Type | Description |
|---|---|---|
customerId | integer | Optional. Internal Casa customer ID. When supplied, isEligible reflects VIP tier matching |
locale | string | Optional. Locale for point-label tokens |
Example request
curl 'https://app.casaloyalty.io/api/external/rewards?customerId=42' \
-H 'Authorization: Bearer cls_live_your_api_key'Success response — HTTP 200
{
"success": true,
"rewards": [
{
"id": 10,
"type": "percentage_discount",
"name": "10% Off Next Order",
"description": "Redeem points for 10% off your next order",
"customTitle": null,
"customSubtitle": null,
"customDescription": null,
"pointsCost": 500,
"discountValue": 10,
"customerGets": "10",
"iconUrl": "https://cdn.example.com/icons/discount.png",
"minRequirement": {
"enabled": true,
"amount": 50
},
"minPoints": {
"enabled": false,
"amount": 0
},
"maxPoints": {
"enabled": false,
"amount": 0
},
"rewardExpiry": {
"enabled": true,
"duration": 30
},
"vipTierRequired": false,
"vipTierId": null,
"isEligible": true,
"placement": 1
}
],
"total": 1
}Field reference
| Field | Description |
|---|---|
type | percentage_discount, fixed_discount, free_shipping, free_product, gift_card, store_credit |
pointsCost | Points required to redeem |
discountValue | Discount amount or percentage depending on type |
minRequirement | Minimum order amount to use the reward |
minPoints | Minimum balance to see/redeem the reward |
maxPoints | Maximum points spendable (dynamic pricing) |
rewardExpiry | Whether redeemed codes expire, and their duration in days |
isEligible | true by default; false when a customerId is supplied and the customer's VIP tier does not match a tier-restricted reward |
isEligible flag evaluates only VIP tier matching. It does not check point balance, blocked status, minimum spend, reward expiry, product availability, or other redemption preconditions. A true value does not guarantee successful redemption.Errors include shared authentication/authorization/rate errors and HTTP 500 INTERNAL_ERROR.
Get Customer by ID
Resource-oriented single customer lookup by Casa internal ID. Returns the same full customer shape as the detail endpoint, including vipTier with threshold and multiplier.
Optional query: locale.
curl 'https://app.casaloyalty.io/api/external/customers/42' \
-H 'Authorization: Bearer cls_live_your_api_key'Success response — HTTP 200
{
"success": true,
"customer": {
"id": 42,
"customerId": "8986033979714",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+16143752923",
"loyaltyPoints": 450,
"loyaltyStatus": "member",
"vipPoints": 850,
"spendings": 1250.5,
"status": "MEMBER",
"dateOfBirth": "1990-05-15T00:00:00.000Z",
"anniversaryDate": "2024-06-20",
"createdAt": "2026-01-01T10:30:00.000Z",
"updatedAt": "2026-08-15T09:22:00.000Z",
"isBlocked": false,
"isDisabled": false,
"vipTier": {
"id": 2,
"name": "Gold",
"minPointsThreshold": 500,
"pointsMultiplier": 1.5
},
"vipTierExpiresAt": "2027-01-01T00:00:00.000Z",
"referral": {
"referralUrl": "abc123XYZ",
"referralCount": 5
}
}
}Errors: 400 VALIDATION_ERROR, 404 CUSTOMER_NOT_FOUND, 500 INTERNAL_ERROR.
Get Reward by ID
Returns a single active reward. Same field structure as the catalog. When customerId is provided, includes full eligibility evaluation with reason codes.
Optional query: locale, customerId (returns eligibility: eligible, reasons[], customerPoints, pointsCost). Reason codes: customer_not_found, customer_blocked, vip_tier_not_eligible, insufficient_points, below_min_points, below_min_spending.
curl 'https://app.casaloyalty.io/api/external/rewards/10?customerId=42' \
-H 'Authorization: Bearer cls_live_your_api_key'Errors: 400 VALIDATION_ERROR, 404 REWARD_NOT_FOUND, 500 INTERNAL_ERROR.
Get VIP Tiers
Returns all VIP tiers ordered by threshold ascending, with active perks and customer count. No pagination.
Optional query: locale.
curl 'https://app.casaloyalty.io/api/external/vip-tiers' \
-H 'Authorization: Bearer cls_live_your_api_key'Each tier contains: id, name, minPointsThreshold, pointsMultiplier, pointsMultiplierEnabled, iconUrl, perks[], customerCount, createdAt.
Errors: shared errors, 500 INTERNAL_ERROR.
Get VIP Tier by ID
Returns a single VIP tier with perks and customer count. Same format as the list endpoint.
| Parameter | Description |
|---|---|
locale | Optional. Locale code for tier name translation (e.g. en, es) |
curl 'https://app.casaloyalty.io/api/external/vip-tiers/2' \
-H 'Authorization: Bearer cls_live_your_api_key'Success response — HTTP 200
{
"success": true,
"vipTier": {
"id": 2,
"name": "Gold",
"minPointsThreshold": 500,
"pointsMultiplier": 1.5,
"pointsMultiplierEnabled": true,
"iconUrl": null,
"perks": [{ "id": 1, "name": "Free shipping" }],
"customerCount": 42,
"createdAt": "2025-01-01T00:00:00.000Z"
}
}Errors: 400 VALIDATION_ERROR, 404 VIP_TIER_NOT_FOUND, 500 INTERNAL_ERROR.
Get Referrals
List referrals with pagination and filtering by status or advocate.
| Parameter | Description |
|---|---|
status | completed, pending, or omit for all |
customerId | Optional. Filter by advocate's internal Casa customer ID |
limit | 1–100 (default 50) |
offset | Non-negative (default 0) |
curl 'https://app.casaloyalty.io/api/external/referrals?status=completed&limit=20' \
-H 'Authorization: Bearer cls_live_your_api_key'Each referral contains: id, advocateCustomerId, referralEmail, status, rewardStatus, orderTotal, currency, createdAt, updatedAt.
Errors: 400 VALIDATION_ERROR, 500 INTERNAL_ERROR.
Get Referral by ID
Returns a single referral with full detail: reward status, reward timing, reward types, and order value.
curl 'https://app.casaloyalty.io/api/external/referrals/101' \
-H 'Authorization: Bearer cls_live_your_api_key'Response includes: id, advocateCustomerId, referralEmail, referralCode, status, rewardStatus, rewardEligibleAt, rewardIssuedAt, rewardCancelledAt, friendRewardType, customerRewardType, orderTotal, currency, createdAt, updatedAt.
Errors: 400 VALIDATION_ERROR, 404 REFERRAL_NOT_FOUND, 500 INTERNAL_ERROR.
Create Referral
Create a referral on behalf of an advocate. Data integrity checks and advocate referral limits are enforced. Browser-dependent fraud checks (IP, household, cookie) are not run — API callers are trusted.
| Field | Type | Required | Description |
|---|---|---|---|
advocateCustomerId | integer | Yes | Internal Casa customer ID of the advocate |
friendEmail | string | Yes | Email of the friend being referred |
Checks performed
- Advocate must exist and not be blocked/disabled
- Referral program must be enabled
- Self-referral rejected (friend email ≠ advocate email)
- Friend must not already be a customer
- No duplicate pending referral for the same friend email
- Advocate referral limit enforced (monthly/lifetime configured limits)
curl -X POST 'https://app.casaloyalty.io/api/external/referrals' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"advocateCustomerId": 42,
"friendEmail": "friend@example.com"
}'Response HTTP 201:
{
"success": true,
"referral": {
"id": 101,
"advocateCustomerId": 42,
"friendEmail": "friend@example.com",
"referralCode": "abc123XYZ",
"status": "pending",
"friendRewardType": "percentage-off",
"customerRewardType": "points",
"createdAt": "2026-08-24T10:00:00.000Z"
}
}Errors: 400 VALIDATION_ERROR, 404 CUSTOMER_NOT_FOUND, 403 CUSTOMER_BLOCKED, 422 REFERRAL_PROGRAM_NOT_CONFIGURED / SELF_REFERRAL / FRIEND_ALREADY_CUSTOMER / REFERRAL_ALREADY_EXISTS / ADVOCATE_LIMIT_REACHED, 500 INTERNAL_ERROR.
Complete Referral
Mark a pending referral as completed and issue rewards. Idempotent — completing an already-completed referral returns success without re-issuing.
| Field | Type | Description |
|---|---|---|
orderTotal | number | Optional. The friend's order total (for reporting) |
orderId | integer | Optional. Link to the order that triggered completion |
friendCustomerId | integer | Optional. Internal Casa ID of the friend customer (required for friend points reward) |
Reward handling
- Points-based advocate rewards are issued immediately (balance credited inline)
- Points-based friend rewards are issued if
friendCustomerIdis provided - Friend store-credit rewards are minted via Shopify if
friendCustomerIdis provided - Non-points advocate rewards may be subject to the shop's reward verification period: if enabled,
rewardStatusis"pending"with a delayedrewardEligibleAt; otherwise eligible immediately - Friend discount codes are deactivated in Shopify after completion (single-use)
- Advocate must not be blocked, disabled, or excluded by tag at completion time (re-checked)
curl -X POST 'https://app.casaloyalty.io/api/external/referrals/101/complete' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"orderTotal": 75.50,
"orderId": 9001,
"friendCustomerId": 55
}'Response HTTP 200:
{
"success": true,
"referral": {
"id": 101,
"advocateCustomerId": 42,
"friendEmail": "friend@example.com",
"status": "completed",
"rewardStatus": "issued",
"orderTotal": 75.50,
"completedAt": "2026-08-24T12:00:00.000Z"
},
"rewards": {
"advocateRewardType": "points",
"advocatePointsAwarded": 200,
"advocateRewardPending": false,
"advocateRewardImmediate": false,
"friendRewardType": "points",
"friendPointsAwarded": 100,
"friendStoreCreditMinted": false
},
"alreadyCompleted": false
}advocateRewardPending: true — safe period active; reward issued after delay. advocateRewardImmediate: true — no safe period; non-points reward eligible immediately. friendStoreCreditMinted: true — friend store-credit minted via Shopify.
Side effects: Klaviyo “Referral Completed” (advocate) + “Referral Friend Completed” (friend), platform events, referralCompleted email, milestone processing.
Errors: 400 VALIDATION_ERROR, 404 REFERRAL_NOT_FOUND, 422 ADVOCATE_NOT_ELIGIBLE (advocate blocked/disabled/excluded), 500 INTERNAL_ERROR.
Block Referral
Block a pending referral from completing. Only works on uncompleted referrals. Idempotent — blocking an already-blocked referral returns success with alreadyBlocked: true.
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | Block reason enum: similar_email, similar_ip, customer_already_exists, advocate_excluded, similar_address, similar_phone_number, advocate_no_prior_orders, disposable_email, similar_customer_name, max_referrals_reached, fraud, other |
details | string | No | Additional details (max 500 chars) |
curl -X PUT 'https://app.casaloyalty.io/api/external/referrals/101/block' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"reason": "fraud",
"details": "Suspected self-referral from same household"
}'Success response — HTTP 200
{
"success": true,
"referral": {
"id": 101,
"advocateCustomerId": 42,
"friendEmail": "friend@example.com",
"status": "blocked",
"reason": "fraud",
"blockedAt": "2026-08-24T14:00:00.000Z"
},
"alreadyBlocked": false
}Errors: 400 VALIDATION_ERROR (invalid reason enum, details too long), 404 REFERRAL_NOT_FOUND, 422 REFERRAL_ALREADY_COMPLETED, 500 INTERNAL_ERROR.
Get Advocate Stats
Returns referral statistics for a customer. The :id is the customer's internal Casa ID.
curl 'https://app.casaloyalty.io/api/external/referrals/42/advocate-stats' \
-H 'Authorization: Bearer cls_live_your_api_key'Success response — HTTP 200
{
"success": true,
"customerId": 42,
"referralUrl": "abc123XYZ",
"stats": {
"completed": 3,
"pending": 1,
"total": 4
}
}Errors: 400 VALIDATION_ERROR, 404 CUSTOMER_NOT_FOUND, 500 INTERNAL_ERROR.
List Redemptions
List reward redemptions with pagination and filtering by customer, status, or reward type.
| Parameter | Description |
|---|---|
customerId | Optional. Filter by internal customer ID |
status | unused, used, expired, or omit for all |
rewardType | Optional. Filter by type (e.g. percentage_discount, gift_card) |
limit | 1–100 (default 50) |
offset | Non-negative (default 0) |
curl 'https://app.casaloyalty.io/api/external/redemptions?customerId=42&status=unused' \
-H 'Authorization: Bearer cls_live_your_api_key'Each redemption: id, rewardId, customerId, rewardType, name, code, usageStatus, fulfillmentState, usageInstructions, termsAndConditions, imageUrl, expiresAt, isExpired, usedAt, createdAt.
Errors: 400 VALIDATION_ERROR, 500 INTERNAL_ERROR.
Create Redemption
Redeem a reward on behalf of a customer. Creates a discount code (or gift card / store credit) and deducts points.
curl -X POST 'https://app.casaloyalty.io/api/external/redemptions' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"customer": { "id": 123 },
"rewardId": 45,
"pointsRange": 500,
"locale": "en"
}'Request body
| Field | Type | Required | Notes |
|---|---|---|---|
customer | object | Yes | Standard identifier: id, customerId, email, or phone |
rewardId | integer | Yes | ID of the reward (from GET /rewards) |
pointsRange | integer | No | For variable-cost (increment) rewards |
locale | string | No | Response text locale (e.g. en, es) |
Success response — HTTP 201
{
"success": true,
"redemption": {
"id": 500,
"rewardId": 45,
"customerId": 123,
"rewardType": "percentage_discount",
"name": "10% Off Next Order",
"code": "CASA-ABC123",
"usageStatus": "unused",
"usageInstructions": "Apply code at checkout",
"termsAndConditions": null,
"expiresAt": "2026-02-01T00:00:00.000Z",
"createdAt": "2026-01-02T16:00:00.000Z"
},
"customer": {
"id": 123,
"loyaltyPoints": 50,
"loyaltyStatus": "member"
}
}Errors: 400 VALIDATION_ERROR, 404 CUSTOMER_NOT_FOUND, 403 CUSTOMER_BLOCKED, 422 REDEMPTION_FAILED, 500 INTERNAL_ERROR.
Submit Activity
Submit an external earning action. Casa evaluates the configured earning rule and awards points if eligible. Supports externalReferenceId for idempotent retries.
curl -X POST 'https://app.casaloyalty.io/api/external/activities' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{"customer": {"email": "john@example.com"}, "actionCode": "product-review", "externalReferenceId": "review_123"}'| Field | Type | Required | Description |
|---|---|---|---|
customer | object | Yes | Customer identifier: id, email, phone, or customerId. Example: {"email": "john@example.com"} |
actionCode | string | Yes | Earning rule code. Example: "product-review", "facebook-like", "instagram-follow" |
externalReferenceId | string | No | Idempotency key. Duplicate submissions return the original result with duplicate: true. Example: "review_abc123" |
description | string | No | Custom activity description (overrides default). Example: "Reviewed product X" |
metadata | object | No | Arbitrary JSON stored with the activity. Example: {"productId": "123"} |
Blocked codes (use dedicated endpoints): place-an-order, signup-bonus, birthday-bonus, anniversary-bonus, daily-visit.
Rules with manualVerify enabled are rejected — use the admin Action Center for those. Points awarded equal the earning rule’s base points (no birthday multiplier is applied via this endpoint).
Errors: 400 VALIDATION_ERROR, 403 CUSTOMER_BLOCKED, 404 CUSTOMER_NOT_FOUND/EARNING_RULE_NOT_FOUND, 422 ALREADY_EARNED/EARNING_LIMIT_REACHED/MANUAL_VERIFY_REQUIRED, 503 ACCOUNTING_TEMPORARILY_UNAVAILABLE.
List Activities
Shop-wide activity list with pagination and filters. Returns non-cancelled activities newest first.
| Parameter | Description |
|---|---|
customerId | Optional. Filter by internal customer ID |
activityType | Optional. points, redemption, order, referral, vip, or omit for all |
source | Optional. Exact source string |
startDate | Optional. ISO datetime (inclusive) |
endDate | Optional. ISO datetime (inclusive) |
limit | 1–100 (default 50) |
offset | Non-negative (default 0) |
curl 'https://app.casaloyalty.io/api/external/activities?activityType=points&limit=20' \
-H 'Authorization: Bearer cls_live_your_api_key'Each activity: id, customerId, type, description, pointsChange, pointsBalance, reason, source, approved, createdAt.
Errors: 400 VALIDATION_ERROR, 500 INTERNAL_ERROR.
Update Customer
Updates a customer's profile fields. Data-only — does NOT trigger entry rewards for dateOfBirth or anniversaryDate. On first write of dateOfBirth/anniversaryDate, stamps the advance-notice latch so the 30-day rule applies to the annual date reward.
curl -X PATCH 'https://app.casaloyalty.io/api/external/customers/42' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{"firstName": "Ada", "lastName": "Lovelace", "phone": "+15551234567", "dateOfBirth": "1990-08-15", "isBlocked": false}'| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | No | Max 100 characters. Example: "Ada" |
lastName | string | No | Max 100 characters. Example: "Lovelace" |
phone | string | null | No | International phone number, or null to clear. Example: "+15551234567" |
dateOfBirth | string | null | No | Date string or null to clear. Does NOT trigger entry rewards. Example: "1990-08-15" |
anniversaryDate | string | null | No | Date string or null to clear. Same behavior as dateOfBirth. Example: "2020-06-01" |
isBlocked | boolean | No | true to block, false to unblock. Blocking prevents earning, redeeming, and referral processing |
At least one field is required. Not updatable: email, loyaltyPoints, vipPoints, spendings, status, isDisabled.
HTTP 200 — returns the full customer object (same shape as GET /customers/:id).
Errors: 400 VALIDATION_ERROR (invalid fields, unknown fields, empty body), 404 CUSTOMER_NOT_FOUND, 500 INTERNAL_ERROR.
Force VIP Tier
Override a customer's VIP tier regardless of their points/spend qualification. Assigns immediately, but automatic tier evaluation may override later based on qualification rules.
curl -X PUT 'https://app.casaloyalty.io/api/external/customers/42/vip-tier' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{"vipTierId": 3, "reason": "Loyalty migration from legacy platform", "expiresAt": "2027-01-01T00:00:00.000Z"}'| Field | Type | Required | Description |
|---|---|---|---|
vipTierId | integer | Yes | Positive integer. ID of the target VIP tier (from GET /vip-tiers). Example: 3 |
reason | string | No | Max 500 characters. Stored in VIP history. Example: "Loyalty migration from legacy platform" |
expiresAt | string | No | ISO 8601 datetime with timezone offset. If omitted, uses the shop’s VIP tier expiry settings. Example: "2027-01-01T00:00:00.000Z" |
If the change is a downgrade (new tier threshold < old tier threshold), rewards from the higher tier are automatically revoked.
HTTP 200: returns customer (full shape), tierChanged (boolean), previousTierId, newTierId, newTierName. If already on the requested tier, returns tierChanged: false with a message.
Errors: 400 VALIDATION_ERROR, 404 CUSTOMER_NOT_FOUND, 404 VIP_TIER_NOT_FOUND, 500 INTERNAL_ERROR.
Get Settings
Returns read-only program configuration for the shop.
curl 'https://app.casaloyalty.io/api/external/settings' \
-H 'Authorization: Bearer cls_live_your_api_key'Response includes points (enabled, singular/plural labels, branding, expiration config), vip (enabled, qualification type, timeframe, start date), and referrals (enabled). Fields may be null when the corresponding program has not been configured.
Errors: 500 INTERNAL_ERROR.
List Translations
Returns all translation locales for the shop. Content is not included in the list response to keep the payload small.
curl 'https://app.casaloyalty.io/api/external/translations' \
-H 'Authorization: Bearer cls_live_your_api_key'Response: { success, translations: [{ id, locale, createdAt, updatedAt }], total }
Errors: 500 INTERNAL_ERROR.
Create Translation
Create a new translation locale.
| Field | Type | Required | Description |
|---|---|---|---|
locale | string | Yes | 1–10 characters. Language code. Must be installed on the Shopify store. Example: "fr", "de", "es" |
content | object | No | Translation content JSON. Default: {}. Example: {"widget": {"title": "Programme de fidélité"}} |
curl -X POST 'https://app.casaloyalty.io/api/external/translations' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{ "locale": "fr", "content": { "widget": { "title": "Programme de fidélité" } } }'Response 201: { success, translation: { id, locale, content, createdAt, updatedAt } }
Errors: 400 VALIDATION_ERROR, 422 TRANSLATION_ALREADY_EXISTS, 500 INTERNAL_ERROR.
Get Translation
Returns full translation content for a specific locale.
curl 'https://app.casaloyalty.io/api/external/translations/fr' \
-H 'Authorization: Bearer cls_live_your_api_key'Response: { success, translation: { id, locale, content, createdAt, updatedAt } }
Errors: 404 TRANSLATION_NOT_FOUND, 500 INTERNAL_ERROR.
Update Translation
Update translation content. Body: { content }. content is required (JSON object).
curl -X PUT 'https://app.casaloyalty.io/api/external/translations/fr' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{ "content": { "widget": { "title": "Programme de fidélité", "subtitle": "Gagnez des points" } } }'Response: { success, translation: { id, locale, content, createdAt, updatedAt } }
Errors: 400 VALIDATION_ERROR, 404 TRANSLATION_NOT_FOUND, 500 INTERNAL_ERROR.
Delete Translation
Delete a translation locale and all its content. The shop's primary locale cannot be deleted (returns 422).
curl -X DELETE 'https://app.casaloyalty.io/api/external/translations/fr' \
-H 'Authorization: Bearer cls_live_your_api_key'Response: { success, deleted: true, locale: "fr" }
Errors: 422 VALIDATION_ERROR (cannot delete primary locale), 404 TRANSLATION_NOT_FOUND, 500 INTERNAL_ERROR.
Get Activity by ID
Retrieve a single non-cancelled activity by ID. Same response shape as a single item from the activities list.
curl 'https://app.casaloyalty.io/api/external/activities/5001' \
-H 'Authorization: Bearer cls_live_your_api_key'Response: { success, activity: { id, type, description, pointsChange, pointsBalance, reason, source, approved, manualVerify, metadata, createdAt } }
Errors: 400 VALIDATION_ERROR (invalid ID), 404 ACTIVITY_NOT_FOUND, 500 INTERNAL_ERROR.
Create Reward
Create a new reward (redemption option) for the shop. Uses a minimal field contract with sensible defaults for all unexposed fields.
Required fields
| Field | Type | Description |
|---|---|---|
type | string | amount-discount, percentage-off, free-shipping, free-product, gift-card, store-credit |
name | string | Reward display name |
pointsCost | integer | Points required to redeem (non-negative) |
discountValue | number | The discount amount or percentage (non-negative) |
Optional fields
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether the reward is active immediately |
expiryDays | integer (1–730) | no expiry | Reward code expiration in days |
minOrderAmount | number | none | Minimum order value required |
vipTierId | integer | none | Restrict to a VIP tier |
iconUrl | string | none | Custom icon URL |
productId | string | — | Required for free-product type |
variantIds | array of strings | — | Variant IDs for free-product |
curl -X POST 'https://app.casaloyalty.io/api/external/rewards' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{
"type": "percentage-off",
"name": "10% Off Next Order",
"pointsCost": 500,
"discountValue": 10,
"expiryDays": 30
}'Response HTTP 201:
{
"success": true,
"reward": {
"id": 15,
"type": "percentage-off",
"name": "10% Off Next Order",
"enabled": true,
"pointsCost": 500,
"discountValue": 10,
"expiryDays": 30,
"minOrderAmount": null,
"vipTierId": null,
"iconUrl": null,
"placement": 3,
"createdAt": "2026-08-24T10:00:00.000Z"
}
}Errors: 400 VALIDATION_ERROR, 500 INTERNAL_ERROR.
Update Reward
Update a reward's configuration. You cannot change type or name after creation. At least one field must be provided.
Updatable fields
| Field | Type | Description |
|---|---|---|
enabled | boolean | Enable/disable the reward |
pointsCost | integer | Update points cost |
discountValue | number | Update discount value |
productId | string | For free-product rewards |
variantIds | array | For free-product rewards |
curl -X PUT 'https://app.casaloyalty.io/api/external/rewards/15' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{ "enabled": false, "pointsCost": 600 }'Response HTTP 200:
{
"success": true,
"reward": {
"id": 15,
"type": "percentage-off",
"name": "10% Off Next Order",
"enabled": false,
"pointsCost": 600,
"discountValue": 10,
"expiryDays": 30,
"minOrderAmount": null,
"vipTierId": null,
"iconUrl": null,
"placement": 3,
"createdAt": "2026-08-24T10:00:00.000Z",
"updatedAt": "2026-08-24T12:00:00.000Z"
}
}Errors: 400 VALIDATION_ERROR, 404 REWARD_NOT_FOUND, 500 INTERNAL_ERROR.
Delete Rewards (Bulk)
Soft-delete one or more rewards by ID.
| Field | Type | Required | Description |
|---|---|---|---|
ids | integer[] | Yes | 1–50 positive integers. Reward IDs to delete. Non-existent or already-deleted IDs are silently skipped. Example: [15, 16, 17] |
curl -X DELETE 'https://app.casaloyalty.io/api/external/rewards' \
-H 'Authorization: Bearer cls_live_your_api_key' \
-H 'Content-Type: application/json' \
-d '{ "ids": [15, 16, 17] }'Response: HTTP 204 (no body).
Errors: 400 VALIDATION_ERROR (invalid/empty IDs, more than 50), 500 INTERNAL_ERROR.
Testing safely
Use a dedicated non-production shop and data set for integration testing. Keys created through the supported Admin workflow are live keys, and every request can read or mutate the authenticated key's shop data.
curl 'https://app.casaloyalty.io/api/external/customer?email=test@example.com' \
-H 'Authorization: Bearer cls_live_your_api_key'Phone Number Format
Phone numbers should be in E.164 international format:
Correct Format
+16143752923(with plus sign)16143752923(without plus sign — will be normalized)
Incorrect Format
614-375-2923(dashes not allowed)(614) 375-2923(parentheses and spaces)6143752923(missing country code)
+ prefix and removing special characters. When passing a phone in a URL query parameter, encode the + as %2B.Best Practices
1. Use unique external order IDs
Always use a unique orderId per order to leverage duplicate detection:
const orderId = `${platform}_${platformOrderId}`;2. Handle existing customers gracefully
The register endpoint returns existing customers with HTTP 200 and status: "existing". Design flows to handle both create and existing paths.
3. Cache customer IDs
Store the internal id from the first registration to use in subsequent lookups — it's the fastest identifier.
4. Use the external flag correctly
external: true— trust your platform's product data (tags, price, etc.)external: false+ validproductId— Casa fetches product/tag data from Shopify for eligibility checks
5. Handle errors properly
Check success and error.code on every response. Key codes to handle: DUPLICATE_ORDER, CUSTOMER_BLOCKED, INSUFFICIENT_POINTS, RATE_LIMIT_EXCEEDED.
6. Security
Storefront App Proxy
For Shopify storefronts, use the App Proxy instead of the External API. Requests are routed through Shopify's servers with automatic HMAC authentication — no API keys needed in the frontend.
When to use which
| Scenario | Use |
|---|---|
| Liquid themes, storefront widgets | App Proxy (/apps/casa-loyalty/*) |
| Mobile apps, external platforms, server-to-server | External API (/api/external/*) |
The App Proxy endpoints (widget data, SDK data, redemption, referrals, activity) are documented separately and require a logged-in Shopify customer — not an API key.