Casa Loyalty External API Unversioned

Current repository-verified API-key reference · base path /api/external

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.

Base URL: 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_key

X-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

  1. Log in to your CasaLoyalty admin dashboard
  2. Navigate to Settings → API Keys
  3. Click Generate API Key and select the desired resource permissions
  4. Copy and securely store your API key — it is shown only once and cannot be retrieved later
Security: Never expose your API key in client-side code or public repositories. Always use environment variables or a secure key management system.

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:action permission (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

ResourceReadWriteEndpoints
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.

HTTPCodes
400VALIDATION_ERROR, SHOPIFY_VALIDATION_ERROR
401MISSING_API_KEY, INVALID_API_KEY
403IP_NOT_WHITELISTED (legacy/operator-configured allowlist; not self-service), INSUFFICIENT_PERMISSIONS, CUSTOMER_BLOCKED
404CUSTOMER_NOT_FOUND
422DUPLICATE_ORDER, EARNING_LIMIT_REACHED, INSUFFICIENT_POINTS
429RATE_LIMIT_EXCEEDED
503ACCOUNTING_TEMPORARILY_UNAVAILABLE; response includes Retry-After: 5
500INTERNAL_ERROR

Error Codes

CodeHTTPDescription
VALIDATION_ERROR400Request validation failed (invalid or missing parameters)
MISSING_API_KEY401No Authorization header supplied
INVALID_API_KEY401Key is unknown, inactive, expired, or invalid
IP_NOT_WHITELISTED403Operator-configured allowlist rejected the IP
INSUFFICIENT_PERMISSIONS403Key lacks the required permission
CUSTOMER_BLOCKED403Customer account is blocked
CUSTOMER_NOT_FOUND404No enabled customer matched the identifiers
EARNING_RULE_NOT_FOUND404No active earning rule with that ID
REWARD_NOT_FOUND404No active reward with that ID
VIP_TIER_NOT_FOUND404No VIP tier with that ID
REFERRAL_NOT_FOUND404No referral with that ID in this shop
DUPLICATE_ORDER422External order ID already processed
EARNING_LIMIT_REACHED422Customer reached the earning limit for this rule
INSUFFICIENT_POINTS422Requested debit exceeds current balance
RATE_LIMIT_EXCEEDED429Per-key hourly request limit exceeded
ACCOUNTING_TEMPORARILY_UNAVAILABLE503Points accounting is temporarily unavailable; retry after Retry-After header
INTERNAL_ERROR500Unhandled 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 /customers
  • GET /customer/orders
  • GET /customer/activity
  • GET /activities
  • GET /referrals
  • GET /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_key

To fetch the next page, use the returned nextCursor:

GET /api/external/customers?limit=20&since_id=520

When 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.
Both modes are non-breaking and can be used interchangeably on the same endpoint. Omitting 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 externalReferenceId rejects duplicates with the original result.
Do not automatically retry a write unless you can determine whether the first mutation committed.

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).

POST/api/external/register-customercustomers:write

Supports the shared optional locale query parameter.

FieldTypeRequiredDescription
firstNamestringYes1–100 characters. Example: "Ada"
lastNamestringYes1–100 characters. Example: "Lovelace"
emailstringConditionalValid email. Required if phone not provided. Example: "ada@example.com"
phonestringConditionalInternational phone number. Required if email not provided. Example: "+16143752923"
dateOfBirthstringNoDate string, no format enforced. Example: "1990-05-15"
initialPointsintegerNoNon-negative. Default: 0. Points credited to new customers only (ignored for existing). Example: 100
metadataobjectNoArbitrary 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.

GET/api/external/customercustomers:read

Supports the shared optional locale query parameter.

GET /api/external/customer?email=john@example.com
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",
    "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.

GET/api/external/customer/ordersorders:read

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.

GET/api/external/customer/activityactivities:read

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.

POST/api/external/process-orderorders:write

Supports the shared optional locale query parameter.

Requires a customer identifier and an order object.

Required order fields

FieldTypeDescription
orderIdstringUnique external order ID (used for duplicate detection)
subtotalnumberNon-negative. Order subtotal before tax/shipping
totalnumberNon-negative. Order total including tax/shipping
currencystring3-letter ISO currency code (e.g. USD, EUR). Auto-uppercased
itemsarrayAt 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

FieldTypeDefaultDescription
orderNamestring#orderIdDisplay name (e.g. #1001)
sourcestringexternal_apiOrder source. Set to pos for POS orders — this triggers the reward channel eligibility check. Any other value is treated as online
isSubscriptionbooleanfalseWhen true, uses the earning rule’s subscription points rate if configured
placedAtstringCurrent timeISO 8601 datetime with timezone offset. When the order was placed on the external platform
metadataobject—Arbitrary JSON stored in the order record
paymentGatewaystring|string[]—Payment gateway(s) used. If any matches the merchant’s excluded list, the order is rejected
taxesnumber—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
shippingnumber—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.

POST/api/external/update-pointspoints:write

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.

Repeating a successful request repeats the mutation; do not automatically retry an ambiguous failure. Accounting commits before non-fatal activity logging, so a successful balance change may lack its corresponding activity row.

Batch Update Points

POST/api/external/update-points/batchpoints:write

Adjust points for multiple customers in a single request. Synchronous processing, max 20 items per call. Per-item results with partial success support.

FieldTypeRequiredDescription
adjustmentsarrayYesArray of adjustment objects (1–20 items)
adjustments[].customerobjectYesStandard identifier: id, customerId, email, or phone
adjustments[].pointsChangeintegerYesNon-zero integer (positive to credit, negative to debit)
adjustments[].reasonstringYesReason for the adjustment (1–500 chars)
adjustments[].applyVipMultiplierbooleanNoApply 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

GET /api/external/customers customers:read

Search, filter, and paginate through customers in your shop. Disabled customers are always excluded from results.

Optional query: locale.

Query parameters

ParameterTypeDescription
searchstringOptional. Substring match across first name, last name, email, and phone
minPointsintegerOptional. Minimum loyalty points (inclusive)
maxPointsintegerOptional. Maximum loyalty points (inclusive)
vipTierIdintegerOptional. Filter to customers in this VIP tier
statusstringGUEST, MEMBER, or all (default all)
isBlockedbooleanOptional. true/1 filters blocked; false/0 filters unblocked
createdAfterISO datetimeOptional. Customers created on or after this date
createdBeforeISO datetimeOptional. Customers created on or before this date
sortBystringloyaltyPoints, vipPoints, spendings, or createdAt (default createdAt)
sortOrderstringasc or desc (default desc)
limitinteger1–100 (default 50)
offsetintegerNon-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

GET /api/external/earning-rules earning_rules:read

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

FieldDescription
codeRule type identifier (e.g. order-purchase, signup-bonus, birthday, daily-visit)
typeAlias of code for backwards compatibility
pointsPoints per qualifying action (fixed) or per increment (incremental)
earningCalculationTypefixed (flat) or incremental (per-unit)
calculationIncrementMonetary/unit increment for incremental calculation
oneTimeIncentiveAdditional bonus on first qualifying action
earningLimitPer-period award limit
vipTierEligibilityTier restriction (include or exclude specific tiers)
manualVerifyWhether earning requires manual approval
signupRewardPresent 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

GET /api/external/rewards rewards:read

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

ParameterTypeDescription
customerIdintegerOptional. Internal Casa customer ID. When supplied, isEligible reflects VIP tier matching
localestringOptional. 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

FieldDescription
typepercentage_discount, fixed_discount, free_shipping, free_product, gift_card, store_credit
pointsCostPoints required to redeem
discountValueDiscount amount or percentage depending on type
minRequirementMinimum order amount to use the reward
minPointsMinimum balance to see/redeem the reward
maxPointsMaximum points spendable (dynamic pricing)
rewardExpiryWhether redeemed codes expire, and their duration in days
isEligibletrue by default; false when a customerId is supplied and the customer's VIP tier does not match a tier-restricted reward
The 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

GET/api/external/customers/:idcustomers:read

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

GET/api/external/rewards/:idrewards:read

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

GET/api/external/vip-tiersvip_tiers:read

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

GET/api/external/vip-tiers/:idvip_tiers:read

Returns a single VIP tier with perks and customer count. Same format as the list endpoint.

ParameterDescription
localeOptional. 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

GET/api/external/referralsreferrals:read

List referrals with pagination and filtering by status or advocate.

ParameterDescription
statuscompleted, pending, or omit for all
customerIdOptional. Filter by advocate's internal Casa customer ID
limit1–100 (default 50)
offsetNon-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

GET/api/external/referrals/:idreferrals:read

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

POST/api/external/referralsreferrals:write

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.

FieldTypeRequiredDescription
advocateCustomerIdintegerYesInternal Casa customer ID of the advocate
friendEmailstringYesEmail 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

POST/api/external/referrals/:id/completereferrals:write

Mark a pending referral as completed and issue rewards. Idempotent — completing an already-completed referral returns success without re-issuing.

FieldTypeDescription
orderTotalnumberOptional. The friend's order total (for reporting)
orderIdintegerOptional. Link to the order that triggered completion
friendCustomerIdintegerOptional. 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 friendCustomerId is provided
  • Friend store-credit rewards are minted via Shopify if friendCustomerId is provided
  • Non-points advocate rewards may be subject to the shop's reward verification period: if enabled, rewardStatus is "pending" with a delayed rewardEligibleAt; 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

PUT/api/external/referrals/:id/blockreferrals:write

Block a pending referral from completing. Only works on uncompleted referrals. Idempotent — blocking an already-blocked referral returns success with alreadyBlocked: true.

FieldTypeRequiredDescription
reasonstringYesBlock 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
detailsstringNoAdditional 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

GET/api/external/referrals/:id/advocate-statsreferrals:read

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

GET/api/external/redemptionsredemptions:read

List reward redemptions with pagination and filtering by customer, status, or reward type.

ParameterDescription
customerIdOptional. Filter by internal customer ID
statusunused, used, expired, or omit for all
rewardTypeOptional. Filter by type (e.g. percentage_discount, gift_card)
limit1–100 (default 50)
offsetNon-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

POST/api/external/redemptionsredemptions:write

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

FieldTypeRequiredNotes
customerobjectYesStandard identifier: id, customerId, email, or phone
rewardIdintegerYesID of the reward (from GET /rewards)
pointsRangeintegerNoFor variable-cost (increment) rewards
localestringNoResponse 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

POST/api/external/activitiesactivities:write

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"}'
FieldTypeRequiredDescription
customerobjectYesCustomer identifier: id, email, phone, or customerId. Example: {"email": "john@example.com"}
actionCodestringYesEarning rule code. Example: "product-review", "facebook-like", "instagram-follow"
externalReferenceIdstringNoIdempotency key. Duplicate submissions return the original result with duplicate: true. Example: "review_abc123"
descriptionstringNoCustom activity description (overrides default). Example: "Reviewed product X"
metadataobjectNoArbitrary 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

GET/api/external/activitiesactivities:read

Shop-wide activity list with pagination and filters. Returns non-cancelled activities newest first.

ParameterDescription
customerIdOptional. Filter by internal customer ID
activityTypeOptional. points, redemption, order, referral, vip, or omit for all
sourceOptional. Exact source string
startDateOptional. ISO datetime (inclusive)
endDateOptional. ISO datetime (inclusive)
limit1–100 (default 50)
offsetNon-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

PATCH/api/external/customers/:idcustomers:write

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}'
FieldTypeRequiredDescription
firstNamestringNoMax 100 characters. Example: "Ada"
lastNamestringNoMax 100 characters. Example: "Lovelace"
phonestring | nullNoInternational phone number, or null to clear. Example: "+15551234567"
dateOfBirthstring | nullNoDate string or null to clear. Does NOT trigger entry rewards. Example: "1990-08-15"
anniversaryDatestring | nullNoDate string or null to clear. Same behavior as dateOfBirth. Example: "2020-06-01"
isBlockedbooleanNotrue 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

PUT/api/external/customers/:id/vip-tiercustomers:write

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"}'
FieldTypeRequiredDescription
vipTierIdintegerYesPositive integer. ID of the target VIP tier (from GET /vip-tiers). Example: 3
reasonstringNoMax 500 characters. Stored in VIP history. Example: "Loyalty migration from legacy platform"
expiresAtstringNoISO 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

GET/api/external/settingssettings:read

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

GET/api/external/translationstranslations:read

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

POST/api/external/translationstranslations:write

Create a new translation locale.

FieldTypeRequiredDescription
localestringYes1–10 characters. Language code. Must be installed on the Shopify store. Example: "fr", "de", "es"
contentobjectNoTranslation 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

GET/api/external/translations/:localetranslations:read

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

PUT/api/external/translations/:localetranslations:write

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/api/external/translations/:localetranslations:write

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

GET/api/external/activities/:idactivities:read

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

POST/api/external/rewardsrewards:write

Create a new reward (redemption option) for the shop. Uses a minimal field contract with sensible defaults for all unexposed fields.

Required fields

FieldTypeDescription
typestringamount-discount, percentage-off, free-shipping, free-product, gift-card, store-credit
namestringReward display name
pointsCostintegerPoints required to redeem (non-negative)
discountValuenumberThe discount amount or percentage (non-negative)

Optional fields

FieldTypeDefaultDescription
enabledbooleantrueWhether the reward is active immediately
expiryDaysinteger (1–730)no expiryReward code expiration in days
minOrderAmountnumbernoneMinimum order value required
vipTierIdintegernoneRestrict to a VIP tier
iconUrlstringnoneCustom icon URL
productIdstring—Required for free-product type
variantIdsarray 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

PUT/api/external/rewards/:idrewards:write

Update a reward's configuration. You cannot change type or name after creation. At least one field must be provided.

Updatable fields

FieldTypeDescription
enabledbooleanEnable/disable the reward
pointsCostintegerUpdate points cost
discountValuenumberUpdate discount value
productIdstringFor free-product rewards
variantIdsarrayFor 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)

DELETE/api/external/rewardsrewards:write

Soft-delete one or more rewards by ID.

FieldTypeRequiredDescription
idsinteger[]Yes1–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)
The API automatically normalizes phone numbers by adding the + 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 + valid productId — 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

Never expose API keys in client-side code or public repositories. Always use server-side requests with environment variables.

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

ScenarioUse
Liquid themes, storefront widgetsApp Proxy (/apps/casa-loyalty/*)
Mobile apps, external platforms, server-to-serverExternal 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.