Skip to content

Loyalty integration

The loyalty integration lets an external loyalty provider keep its members’ cards in KatalystOS, so the restaurant’s staff and receipts see the same card. Your system owns the card number; KatalystOS stores the card under the restaurant’s loyalty program and keeps punches, rewards and a units ledger on it.

All endpoints are under /v1/integration/loyalty/ and need a token with the integration.loyalty permission. Examples use the sandbox and assume $T holds your token.

Send your card number and whatever cardholder details you have. The number is the card’s identity within the program.

Terminal window
curl -X POST https://dev-api.katalystos.com/v1/integration/loyalty/card/ \
-H "X-KATALYST-AUTH-TOKEN: $T" -H "Content-Type: application/json" \
-d '{
"number": "WD-100234",
"cardholder_email": "ana@example.com",
"cardholder_phone": "(555) 123-4567",
"cardholder_first_name": "Ana",
"cardholder_last_name": "Diaz"
}'
{
"success": true,
"item": {
"_id": "6650f0c2a1b2c3d4e5f60718",
"number": "WD-100234",
"physical_card_number": null,
"provider": "wedu",
"is_active": true,
"balance": 0,
"points": 0,
"points_lifetime": 0,
"units": null,
"punches": null,
"cardholder_email": "ana@example.com",
"cardholder_phone": "5551234567",
"cardholder_first_name": "Ana",
"cardholder_last_name": "Diaz"
}
}

(Envelope fields are left out of the examples on this page.)

  • Idempotent on number. If a card with that number already exists in the program you get it back unchanged; the cardholder fields you sent are not applied to it. Retrying a timed-out create is safe.
  • Keep the _id. The card endpoints below address the card by _id.
  • Phone numbers are stored as digits only; emails are stored trimmed and in lower case.
  • provider defaults to wedu, the only external provider today.

Look a card up by exactly one of number, cardholder_phone or cardholder_email:

Terminal window
curl "https://dev-api.katalystos.com/v1/integration/loyalty/card/?cardholder_phone=555-123-4567" \
-H "X-KATALYST-AUTH-TOKEN: $T"

If you send more than one, number wins, then phone, then email. A phone or email that matches more than one card fails with Multiple cards match; look the card up by number.; use the number.

The number is meant to be stable. If one was saved with a typo, replace it:

  1. With the card id: PUT /v1/integration/loyalty/card/{card_id}/ with {"number": "WD-100243"}.

  2. Without it, by the cardholder’s email: PUT /v1/integration/loyalty/card/ with {"cardholder_email": "ana@example.com", "number": "WD-100243"}. This fails if the email matches more than one card.

Both fail with This card number is already in use. if another card in the program has the new number. Past operations keep the number they were recorded with.

A restaurant can run punch-card programs (“buy 9 coffees, the 10th is free”). KatalystOS gives you the punch_card_program_id when one is set up for your integration. Add punches for purchases made in your system:

Terminal window
curl -X POST https://dev-api.katalystos.com/v1/integration/loyalty/card/6650f0c2a1b2c3d4e5f60718/punch/ \
-H "X-KATALYST-AUTH-TOKEN: $T" -H "Content-Type: application/json" \
-d '{"punch_card_program_id": "6650f1d9a1b2c3d4e5f60720", "count": 1}'
{
"success": true,
"punches": 4,
"target_count": 10,
"item": { "_id": "6650f0c2a1b2c3d4e5f60718", "punches": { "6650f1d9a1b2c3d4e5f60720": 4 } }
}

count is 1 to 100 (default 1). When the card reaches target_count the program’s reward is issued automatically. List the card’s rewards, newest first:

Terminal window
curl "https://dev-api.katalystos.com/v1/integration/loyalty/card/6650f0c2a1b2c3d4e5f60718/rewards/?status=issued" \
-H "X-KATALYST-AUTH-TOKEN: $T"
{
"success": true,
"items": [
{
"_id": "6651a0b3a1b2c3d4e5f60801",
"code": "K7QH2M",
"name": "Free coffee",
"type": "free_item",
"value": null,
"source": "punch_card",
"status": "issued",
"expires_at": "2026-12-31 23:59:59",
"redeemed_at": null,
"created_at": "2026-11-06 09:12:44"
}
]
}

status is one of issued, redeemed, expired, revoked; leave it out to get all of them. The list returns at most 200 rewards.

Units are a ledger of free-form quantities per card: gallons of fuel, kWh, miles, anything your program counts. They are stored only; they do not change points or balance.

Record units. Values are deltas, keyed by a unit code you choose:

Terminal window
curl -X POST https://dev-api.katalystos.com/v1/integration/loyalty/card/6650f0c2a1b2c3d4e5f60718/units/ \
-H "X-KATALYST-AUTH-TOKEN: $T" -H "Content-Type: application/json" \
-d '{"units": {"gallons_diesel": 12.4}}'

The response is the card, with units holding the running total per code. One call is one ledger entry, even when it carries several codes.

  • A code cannot be empty, contain ., or start with $.
  • A negative value corrects an earlier entry (send -9 to undo a 10 that should have been 1), but no total may go below zero: Correction would take 'gallons_diesel' below zero.

Read the history, newest first, with limit (1 to 200, default 50) and offset:

Terminal window
curl "https://dev-api.katalystos.com/v1/integration/loyalty/card/6650f0c2a1b2c3d4e5f60718/units/?limit=20" \
-H "X-KATALYST-AUTH-TOKEN: $T"
{
"success": true,
"totals": { "gallons_diesel": 12.4 },
"items": [
{
"_id": "6651b2c4a1b2c3d4e5f60902",
"operation": "accrue_units",
"units": { "gallons_diesel": 12.4 },
"units_start": {},
"units_end": { "gallons_diesel": 12.4 },
"payment_id": null,
"check_id": null,
"restaurant_id": "64f0a1b2c3d4e5f607182930",
"created_at": "2026-11-06 09:20:11"
}
],
"total": 1
}

Fix an entry in place instead of posting a correction. amount is that entry’s own change, not the card’s total:

Terminal window
curl -X PUT https://dev-api.katalystos.com/v1/integration/loyalty/card/6650f0c2a1b2c3d4e5f60718/units/6651b2c4a1b2c3d4e5f60902/ \
-H "X-KATALYST-AUTH-TOKEN: $T" -H "Content-Type: application/json" \
-d '{"code": "gallons_diesel", "amount": 1.24}'

Send new_code to move the entry to another code, amount to change it, or both. An amount of 0 is rejected; delete the entry instead:

Terminal window
curl -X DELETE https://dev-api.katalystos.com/v1/integration/loyalty/card/6650f0c2a1b2c3d4e5f60718/units/6651b2c4a1b2c3d4e5f60902/ \
-H "X-KATALYST-AUTH-TOKEN: $T"

Deleting removes every code in that entry. Edits and deletes are refused if a total would go below zero; remove the later correction first.

WEDU integrations have one more endpoint, which needs the integration.wedu permission. POST /v1/integration/wedu/loyalty-user/ finds or creates the diner by email or phone, links them to the restaurant, and creates their card if they do not have one yet:

{
"email": "ana@example.com",
"phone": "5551234567",
"first_name": "Ana",
"last_name": "Diaz",
"external_card_number": "WD-100234"
}

phone is exactly 10 digits. The response has the diner as user and the card as card.

Message (in errors) Cause
No loyalty program found for this restaurant. The restaurant has no loyalty program, or restaurant_id is wrong.
Loyalty card not found. No card with that id or lookup value in this program.
Multiple cards match... Look the card up by number.
This card number is already in use. Another card in the program has the number.
Punch card program not found. Wrong id, another program’s id, or the program is inactive.
Units entry not found. The operation_id is not a units entry of this card.

All of these are HTTP 200 with success: false. See Responses and errors.