☰ Menu · Kiosks and scanners
Kiosks and scanners
Devices without a till: a scanner at the entrance, a tablet where guests sign up, a counter scanner that gives out rewards.
Give each device its own key with only the devices:write scope. It can scan cards, record visits, sign guests up and give out rewards, but it cannot list members or send checks. If the tablet is stolen, revoke that one key.
Scan anything
Send exactly what the scanner produced to POST /v1/scans. We handle the different shapes for you: a card number, a card QR, a Code 128 with the ]C1 symbology prefix, a trailing Enter or Tab from a keyboard-wedge scanner, a link to the card page, or a phone number typed on a kiosk.
curl https://loyaltyfy.io/api/v1/scans \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "code": "]C14285324286429\r", "device_id": "door-1" }'{
"object": "scan",
"matched": true,
"matched_by": "4285324286429",
"device_id": "door-1",
"mechanic": "stamps",
"member": {
"object": "member",
"card_number": "4285324286429",
"first_name": "Ana",
"stamps": { "current": 1, "required": 5, "rewards_available": 0 },
...
},
"actions": [
{ "type": "visit", "method": "POST", "path": "/v1/visits" },
{ "type": "transaction", "method": "POST", "path": "/v1/transactions" }
]
}actions lists what this device can do next for this guest, so the screen can show the right buttons without knowing the program's rules. When the guest has a reward waiting, a redeem_reward action appears with the path to call.
"actions": [
{ "type": "visit", "method": "POST", "path": "/v1/visits" },
{ "type": "transaction", "method": "POST", "path": "/v1/transactions" },
{
"type": "redeem_reward",
"method": "POST",
"path": "/v1/members/4285324286429/rewards/redeem",
"available": 1
}
]Stamp a visit without a check
For stamp and visit programs, a check-in is enough: a gym door, a car wash bay, a coffee counter that does not run a till. POST /v1/visits gives the per-visit reward of the program. Send a unique external_id for each check-in (for example the device name plus a counter); a repeat is ignored.
curl https://loyaltyfy.io/api/v1/visits \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "door-1-2026-10-06-0042",
"member": "4285324286429",
"device_id": "door-1"
}'{
"object": "visit",
"id": "feb2ad75-ac7b-4bb6-8b64-192323b7f6ea",
"external_id": "door-1-2026-10-06-0042",
"status": "completed",
"amount": 0,
"stamps_earned": 1,
"reward_unlocked": false,
"register_id": "door-1",
"livemode": false,
"created_at": "2026-10-06T18:17:28.209+00:00",
"member": {
"object": "member",
"card_number": "4285324286429",
"stamps": { "current": 2, "required": 5, "rewards_available": 0 },
...
},
...
}400 visit_not_supported. The actions of a scan never offer visit for such members.To avoid stamping twice when a guest scans twice in a row, build the external_id from the member and the time window, for example door-1-4285324286429-2026-10-06T18: one stamp per hour at most.
Sign-up kiosk
A tablet at the entrance asks for a phone and a name, shows the privacy terms, and calls POST /v1/members with consent: true only after the guest agrees. Then show wallet.apple_url or wallet.google_url as a QR: the guest scans it and the card goes straight into their wallet.
curl https://loyaltyfy.io/api/v1/members \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: kiosk-3-0193" \
-d '{ "phone": "+306912345678", "first_name": "Eleni", "language": "el", "consent": true }'
# show response.wallet.apple_url or google_url as a QR on the kiosk screenIf the phone is already enrolled, the call returns the existing member with 200, so a guest who lost the card simply gets it again.