Included in the Loyalisto plan
Stamp a card from the system you already run.
Your point of sale, your booking software or your back office can read the balances on your loyalty cards and move them. The wallet pass on your customer’s phone updates afterwards on its own. There is nothing for them to open and nothing for you to trigger.
- Base URL
- https://api.loyalisto.com
- Machine-readable
- /api/openapi.jsonOpenAPI 3.1
Authentication
Mint a key in Settings → API and webhooks. It begins with loy_ and is scoped to your business alone. Present it either way.
A key opens the endpoints on this page and nothing else: not billing, not staff, not your branding, not an export of your customer list. It is a long-lived credential that lives inside somebody else’s software, so it is scoped for that.
curl https://api.loyalisto.com/api/v1/customers \
-H "x-api-key: loy_..."curl https://api.loyalisto.com/api/v1/stamp \
-H "Authorization: Bearer loy_..." \
-H "Content-Type: application/json" \
-d '{"qrCode":"abc123xyz"}'When it goes wrong
- 400
- The body or a parameter is not valid, the request leaves out something the program needs (spendAmount on a points program), or the program refused it: a full stamp card, the day’s stamp limit, a spend below the minimum, or more points than the card holds.
- 401
- The key is missing, revoked or expired.
- 402
- The account is suspended, is not live yet because its free trial has not started, or its subscription has ended. The message says which, and nothing on the card moves.
- 403
- This key may not reach this route, or the card belongs to another business.
- 404
- No such card, or no reward waiting to be given on it.
- 409
- The card changed while you were writing to it, or the reward was already given. Read it again and retry.
- 410
- The card is no longer active, its program has ended, the card has expired, or the reward was not claimed in time.
- 429
- Rate limited.
One answer is not an error. A stamp sent within 30 seconds of the last stamp on the same card answers 200 with status: "duplicate" and adds nothing, so a double scan at the counter counts once. Spending points does not start that window, so a purchase recorded right after a redemption counts. To record two purchases on one card, send the second at least 30 seconds after the first or, on a points program, send one call with their combined spendAmount.
Endpoints
/api/v1/customersList the customers holding a card
Every customer enrolled in one of your programs, with their current balance. Paginated, newest activity first.
Parameters
pagequery- Page number. Defaults to 1.
limitquery- Rows per page, up to 100. Defaults to 20.
searchquery- Match on name, email or phone.
Response
{
"data": [
{
"id": "card_8sk2md",
"customerId": "cus_2ja91z",
"name": "Amina Ben Salah",
"email": "amina@example.com",
"phone": "+12025550143",
"stampCount": 7,
"pointBalance": 0,
"visitCount": 7,
"referralCount": 0,
"tier": null,
"lastActivityAt": "2026-09-12T09:31:04.000Z",
"enrolledAt": "2026-06-02T14:05:00.000Z",
"program": {
"name": "Coffee card",
"type": "STAMP_CARD",
"config": {
"stampsRequired": 10
}
},
"customFields": null
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}/api/v1/customers/{id}Read one customer card
The card, its holder, its program with the rewards it offers, and the last ten rewards given on it. The id is the card id returned by the list; the card’s balance history is the transactions route below.
Parameters
idpath · required- Card id.
Response
{
"card": {
"id": "card_8sk2md",
"qrCode": "ABCD2345WXYZ",
"stampCount": 7,
"pointBalance": 0,
"visitCount": 7,
"referralCount": 0,
"referralCode": null,
"lifetimePoints": 0,
"lifetimeSpend": 0,
"tier": null,
"enrolledAt": "2026-06-02T14:05:00.000Z",
"lastActivityAt": "2026-09-12T09:31:04.000Z",
"isActive": true,
"blockedAt": null
},
"customer": {
"id": "cus_2ja91z",
"firstName": "Amina",
"lastName": "Ben Salah",
"email": "amina@example.com",
"phone": "+12025550143",
"birthDate": null
},
"program": {
"id": "prg_3kf81d",
"name": "Coffee card",
"type": "STAMP_CARD",
"config": {
"stampsRequired": 10
}
},
"rewards": [
{
"id": "rwd_91kd2x",
"name": "Free coffee",
"triggerType": "STAMP_COUNT",
"triggerValue": 10
}
],
"redemptions": [
{
"id": "red_5ma0qe",
"code": "cm0f3k9qa0007l8vxq7xk2mzd",
"rewardName": "Free coffee",
"redeemedAt": "2026-08-30T16:12:40.000Z",
"isUsed": true,
"usedAt": "2026-08-30T16:14:02.000Z"
}
],
"customFields": null
}/api/v1/customers/{id}/transactionsList one card’s transactions
Everything that has moved this card’s balance, most recent first.
Parameters
idpath · required- Card id.
pagequery- Page number. Defaults to 1.
limitquery- Rows per page, up to 100. Defaults to 50.
typequery- Only transactions of this type: one of STAMP_ADD, STAMP_REMOVE, POINTS_EARN, POINTS_REDEEM, VISIT_RECORD, CASHBACK_EARN, CASHBACK_REDEEM, TIER_CHANGE, ADJUSTMENT. Any other value answers 400.
Response
{
"data": [
{
"id": "txn_44kd0a",
"type": "STAMP_ADD",
"value": 1,
"spendAmount": null,
"note": null,
"channel": "QR_SCAN",
"staffName": null,
"createdAt": "2026-09-12T09:31:04.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 1,
"totalPages": 1
}
}/api/v1/scan/{qrCode}Look up a card by the code on it
Resolves the code a point of sale or scanner just read into the card it belongs to, with the balance and what the next reward needs. A card belonging to another business answers 403, never a balance.
Parameters
qrCodepath · required- The code carried by the customer’s card.
Response
{
"currency": "USD",
"card": {
"id": "card_8sk2md",
"qrCode": "ABCD2345WXYZ",
"stampCount": 7,
"pointBalance": 0,
"visitCount": 7,
"referralCount": 0,
"tier": null,
"expiresAt": null,
"lastActivityAt": "2026-09-12T09:31:04.000Z"
},
"customer": {
"firstName": "Amina",
"lastName": "Ben Salah",
"name": "Amina Ben Salah"
},
"program": {
"id": "prg_3kf81d",
"name": "Coffee card",
"type": "STAMP_CARD",
"config": {
"stampsRequired": 10
}
},
"reward": {
"rewardReady": false,
"rewardsAvailable": [],
"nearReward": false,
"stampsAway": 3,
"nearest": {
"rewardId": "rwd_91kd2x",
"triggerType": "STAMP_COUNT",
"triggerValue": 10,
"currentValue": 7,
"away": 3
}
}
}/api/v1/stampRecord a visit
Adds stamps or points to the card behind a code. The wallet pass updates on the customer’s phone shortly afterwards; the call does not wait for it. Send spendAmount for a program that earns per unit spent. A second call within 30 seconds of the last stamp or points scan recorded on the card (spending points does not count) adds nothing and answers 200 with status "duplicate" and the balances unchanged, so a double scan at the counter counts once. When a reward left unclaimed past the window its merchant set lapsed on this scan, lapsedRewards names it, and message says why a stamp card started again. During a business’s launch week, the 7 days after its first customer joined, a stamp-card visit earns double when the business has launch-week double stamps on; it never stacks with a double-stamp day, and the daily stamp limit still applies. launchWeek is true when launch week is what added the extra stamps to this visit.
Body
Required: qrCode
{
"qrCode": {
"type": "string",
"description": "The code carried by the customer’s card."
},
"spendAmount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "What they spent, in the shop’s own currency. Required by programs that earn per unit spent."
},
"category": {
"type": "string",
"description": "Category name, for a program with bonus categories."
},
"note": {
"type": "string",
"description": "Free text kept on the transaction."
},
"timeZone": {
"type": "string",
"description": "The shop’s IANA time zone, such as America/Chicago. Double-stamp days and the daily stamp limit follow the shop’s calendar; without it they follow US Eastern time."
}
}Response
{
"status": "success",
"transaction": {
"id": "txn_44kd0a",
"type": "STAMP_ADD",
"value": 1
},
"card": {
"stampCount": 8,
"pointBalance": 0,
"visitCount": 8,
"tier": null
},
"rewardTriggered": false,
"rewardName": null,
"redemption": null,
"reward": {
"rewardReady": false,
"rewardsAvailable": [],
"nearReward": true,
"stampsAway": 2,
"nearest": {
"rewardId": "rwd_91kd2x",
"triggerType": "STAMP_COUNT",
"triggerValue": 10,
"currentValue": 8,
"away": 2
}
},
"lapsedRewards": [],
"message": null,
"launchWeek": false
}/api/v1/stamp/redeemGive a reward
Marks a reward as handed over and resets whatever the program resets.
Body
Required: cardIdrewardId
{
"cardId": {
"type": "string",
"description": "Card id."
},
"rewardId": {
"type": "string",
"description": "The reward being given."
}
}Response
{
"status": "redeemed",
"redemption": {
"id": "red_91kd2x",
"code": "cm0g1p4rt0002m3wqa4f2qkzj",
"rewardName": "Free coffee",
"usedAt": "2026-09-12T09:31:04.000Z"
}
}/api/v1/stamp/redeem-pointsSpend points
Takes points off a card, for a program where points buy something priced in points. cashValue is what the points take off the bill, in the currency named beside it. euroValue is a deprecated name for the same amount, kept for integrations that read it; new code reads cashValue.
Body
Required: cardIdpoints
{
"cardId": {
"type": "string",
"description": "Card id."
},
"points": {
"type": "integer",
"minimum": 1,
"description": "How many points to take off."
}
}Response
{
"status": "success",
"pointsSpent": 100,
"cashValue": 1,
"currency": "USD",
"newBalance": 120
}Need the API?
The REST API is included in the Loyalisto plan. You can see your own card and set your program up before you pay for it.