#Loyalty history
POST /loyalty-history
Write a member's loyalty history from the platform you are leaving: earns, spends and adjustments, each filed under the time it happened. Rolling tier windows and monthly activity then see real history instead of starting from zero. Entries may arrive in any order and may predate the member joining; the balance and every entry's running total are put back in order before the call returns. Sending an externalId again writes nothing a second time. A member who already has an opening balance from a member import has it reduced by what the history explains, so nothing is counted twice.
Requires the loyalty:write permission.
Costs 10 rate limit tokens instead of the usual one. See Rate limits.
Body Parameters
dataobjectrequiredThe member's history to write.
Request
curl \
-X POST \
"https://api.cascade.dev/loyalty-history" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"customerEmail": "[email protected]",
"loyaltyProgramId": "550e8400-e29b-41d4-a716-446655440000",
"entries": [
{
"externalId": "...",
"occurredAt": "...",
"amount": "...",
"kind": "...",
"reason": "..."
}
]
}
}'Request
import { client } from "@cascade-commerce/api/admin/client"
import { postLoyaltyHistory } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postLoyaltyHistory({
body: {
data: {
customerEmail: "[email protected]",
loyaltyProgramId: "550e8400-e29b-41d4-a716-446655440000",
entries: [
{
externalId: "...",
occurredAt: "...",
amount: "...",
kind: "...",
reason: "...",
},
],
},
},
})Request
require "net/http"
uri = URI("https://api.cascade.dev/loyalty-history")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"data": {
"customerEmail": "[email protected]",
"loyaltyProgramId": "550e8400-e29b-41d4-a716-446655440000",
"entries": [
{
"externalId": "...",
"occurredAt": "...",
"amount": "...",
"kind": "...",
"reason": "..."
}
]
}
}
JSON
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyRequest
import json
from urllib.request import Request, urlopen
body = """{
"data": {
"customerEmail": "[email protected]",
"loyaltyProgramId": "550e8400-e29b-41d4-a716-446655440000",
"entries": [
{
"externalId": "...",
"occurredAt": "...",
"amount": "...",
"kind": "...",
"reason": "..."
}
]
}
}"""
req = Request(
"https://api.cascade.dev/loyalty-history",
data=body.encode(),
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res))Response
{
"customerId": "550e8400-e29b-41d4-a716-446655440000",
"loyaltyProgramId": "550e8400-e29b-41d4-a716-446655440000",
"loyaltyMembershipId": "550e8400-e29b-41d4-a716-446655440000",
"createdCount": 0,
"duplicateCount": 0,
"balance": 0,
"loyaltyTierId": "550e8400-e29b-41d4-a716-446655440000",
"openingAdjustment": {
"status": "none",
"amount": 0
},
"entries": [
{
"externalId": "string",
"status": "created",
"loyaltyTransactionId": "550e8400-e29b-41d4-a716-446655440000",
"occurredAt": "2025-01-15T09:30:00Z",
"amount": 0,
"balanceAfter": 0
}
]
}Response schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"customerId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The customer the history was written against."
},
"loyaltyProgramId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The program the history was written into."
},
"loyaltyMembershipId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The membership the entries belong to. The customer is enrolled if they were not."
},
"createdCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Entries this call wrote."
},
"duplicateCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Entries already on the ledger from an earlier call, which were left alone."
},
"balance": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "The member's points balance once the history was in order."
},
"loyaltyTierId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
],
"description": "The tier the member sits in after the import. Imported history counts toward tier thresholds, so a member can be promoted by it."
},
"openingAdjustment": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["none", "unchanged", "reduced", "removed"],
"description": "What happened to the opening adjustment a balance import left behind: reduced by what the history explains, removed when the history explains all of it, or unchanged when this call wrote nothing new."
},
"amount": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "What is left of the opening adjustment, or null when there is none."
}
},
"required": ["status", "amount"],
"additionalProperties": false,
"description": "How this import squared with the opening adjustment from a balance import. The member's balance never moves because of it."
},
"entries": {
"type": "array",
"items": {
"type": "object",
"properties": {
"externalId": {
"type": "string",
"description": "The identifier you sent."
},
"status": {
"type": "string",
"enum": ["created", "duplicate"],
"description": "Whether this call wrote the entry, or found it already on the ledger."
},
"loyaltyTransactionId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The ledger entry, new or existing."
},
"occurredAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "The timestamp the entry is filed under."
},
"amount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "The points the entry moved."
},
"balanceAfter": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "The member's balance immediately after this entry, once the history was in order."
}
},
"required": [
"externalId",
"status",
"loyaltyTransactionId",
"occurredAt",
"amount",
"balanceAfter"
],
"additionalProperties": false
},
"description": "One result per entry you sent, in the same order."
}
},
"required": [
"customerId",
"loyaltyProgramId",
"loyaltyMembershipId",
"createdCount",
"duplicateCount",
"balance",
"loyaltyTierId",
"openingAdjustment",
"entries"
],
"additionalProperties": false
}