Reporting custom loyalty events

Report an event with POST /loyalty-events. It takes your secret API key like every other admin endpoint, so call it from your server and never from storefront JavaScript.

curl -X POST https://your-store.meisei.app/api/loyalty-events \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "review.written",
    "customerEmail": "ada@example.com",
    "properties": { "wordCount": 84, "hasMedia": true },
    "idempotencyKey": "review-88213"
  }'
Response (202 Accepted)
{
  "event": {
    "id": "k3Fq9TzB",
    "customerId": "Vb7Lm2Qx",
    "name": "review.written",
    "value": null,
    "properties": { "wordCount": 84, "hasMedia": true },
    "idempotencyKey": "review-88213",
    "occurredAt": "2026-10-12T09:30:00Z",
    "createdAt": "2026-10-12T09:30:00Z"
  },
  "duplicate": false,
  "loyaltyProgramIds": ["Pq4Rt8Ws"],
  "pausedLoyaltyProgramIds": []
}

A 202 means the event was recorded, not that anything was awarded. Rules run on a queue, so a slow rule never holds up your checkout. loyaltyProgramIds names the programs with a rule listening for the name; whether the customer earns is up to that rule's conditions and caps.

FieldWhat it is
nameRequired. Letters, digits, periods, underscores, colons and hyphens, up to 100 characters, starting with a letter or digit.
customerId or customerEmailRequired, exactly one of them.
propertiesA flat object of strings, numbers and booleans, at most 50. Names start with a letter and use letters, digits and underscores, up to 64 characters.
valueA whole number of units, for rules that scale. See below.
idempotencyKeyYour own identifier for the event, so a retry awards nothing twice.
occurredAtWhen it happened, as ISO 8601. Defaults to now.
createCustomerCreate the customer when the email is one Meisei has never seen.

Conditions, punch cards and limits

On the rule, the trigger is { "type": "customEvent", "eventName": "review.written" }, plus any of these:

  • conditions, keyed by property name, such as { "wordCount": { "$gte": 20 }, "channel": "store" }. A bare value means equality; the operators are $eq, $ne, $in, $gt, $gte, $lt, $lte and $exists. Every condition has to hold, at most 10. Types are strict, so 5 does not match "5" and true does not match 1. Send each property with one type. A missing property passes $ne.
  • every: fire on the customer's Nth, 2Nth, 3Nth matching event. Members' progress is in the widget API as member.ruleProgress.
  • limit: { "period": "day" | "week" | "month", "count": 2 } caps how often the rule pays one customer per calendar period, in the organization's time zone.
  • variableAmount and defaultValue: scale the reward by the event's value, so a 50-point rule pays 150 for a value of 3. An event with no value uses defaultValue, or 1. Only points, fixed-amount coupons and gift cards can scale.

Retries, unknown customers and paused programs

  • Retries. Sending the same idempotencyKey again for the same customer returns the original event with "duplicate": true and awards nothing again. Keys are per customer, so two of your systems reusing one record id don't collide. Leave the key out and every call counts.
  • Unknown customers. An email Meisei has never seen is a 404 that names the address, so a typo doesn't create a customer. Send "createCustomer": true when your system is where customers sign up.
  • Paused programs earn nothing, so they're listed in pausedLoyaltyProgramIds rather than queued. This isn't an error.

Listing events

GET /loyalty-events returns events newest first. Filter on name, properties.<name>, customer.id, customer.email or occurredAt, search by customer, and add include=customer for the customer's details. GET /loyalty-events/{id} returns one.

Rate limits and webhooks

POST /loyalty-events allows a burst of 120 calls per organization, refilling at two a second, on top of the organization-wide limit. Back off and retry on a 429. For backfilling balances or history, use the member import or the history import instead, because an event sent here earns under today's rules.

What an event earns arrives as loyaltyTransaction.created and loyaltyReward.issued webhooks. Full schemas are in the API reference: Loyalty events and Loyalty rules.

What a merchant sets up, and the rules they write, are in Custom loyalty events.