Give loyalty from your POS
The cashier rings up the sale as usual; your POS adds the stamps or points to the customer's ServeU card. The key needs Give stamps and points (loyalty:write, which includes looking cards up). Give each branch's POS its own key limited to that branch.
1. Find the customer
By phone (Saudi local format or E.164; other countries' E.164 numbers work too):
curl -X POST https://serveu-psi.vercel.app/api/v1/cards/lookup -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-d '{"phone":"0551234567"}'Or scan the customer's card QR and send what the scanner reads (the 24-digit card number or the card page URL) as card_token: {"card_token":"<scanned text>"}. Send exactly one of the two.
The answer is the card. Keep its id and read program.type: stamps or points.
2. Not found? Offer to join
404 not_found means this phone has no card here yet. With the customer's OK, join them:
curl -X POST https://serveu-psi.vercel.app/api/v1/cards -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-d '{"phone":"0551234567","name":"Noura"}'The answer is always 201 with { "existing": …, "card": … }: existing: true means the customer already had a card (say, they joined a moment ago from a poster), and you get that card. The new card is unverified until the customer confirms their phone.
3. After payment, add stamps or points
Stamps program: send stamps. Points program: send bill_amount in halalas (100 = 1.00 SAR) and ServeU works out the points.
serve action) its stamps or points are added automatically, so don't call /earn for it too, or the customer gets them twice.curl -X POST https://serveu-psi.vercel.app/api/v1/cards/<card id>/earn -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: receipt-10422" \
-d '{"stamps":1,"reference":"R-10422"}'
# a points program:
curl -X POST https://serveu-psi.vercel.app/api/v1/cards/<card id>/earn -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: receipt-10423" \
-d '{"bill_amount":4400,"reference":"R-10423"}'referenceis your receipt number; it shows in the card's history.Idempotency-Key: receipt-<number>makes a retry safe (see Idempotency).- The answer (
201) has theevent(keep itsidfor refunds) and the updatedcard.capped: truemeans the per-visit cap reduced the points. - Print
card.balanceandcard.progresson the receipt (stamps_to_nextorpoints_to_next, andrewards_available).
4. Redeem a reward
Stamps program: redeem one full card. The body has nothing required, but it must still be JSON: send {} or just a reference.
curl -X POST https://serveu-psi.vercel.app/api/v1/cards/<card id>/redeem -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: redeem-R-10424" \
-d '{"reference":"R-10424"}'Points program: send a reward_id from card.rewards (the ones the customer can afford now are in card.progress.affordable_reward_ids):
curl -X POST https://serveu-psi.vercel.app/api/v1/cards/<card id>/redeem -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: redeem-R-10425" \
-d '{"reward_id":"<reward id>","reference":"R-10425"}'5. Refund or mistake: undo
curl -X POST https://serveu-psi.vercel.app/api/v1/loyalty-events/<event id>/void -H "Authorization: Bearer serveu_live_XXXX…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: void-R-10422" \
-d '{"reason":"Refund of R-10422"}'The event stays in the card's history, struck through, and the balance goes back. If a retry answers 409 already_voided, the first attempt worked: treat it as success.
Answers to show the cashier
These are normal outcomes, not bugs:
card_cap_reached: more stamps than the program allows per visit.below_min_bill: the bill is under the points program's minimum.not_enough_balance: the customer can't afford that reward yet.no_program: the business has no active loyalty program.
Each of these answers is stored with its Idempotency-Key for 24 hours. If the cashier corrects the sale (fewer stamps, another reward) or the owner sets up a program, send the corrected request with a new key, e.g. receipt-10422-2: the old key would answer the stored error again (or 409 idempotency_conflict if the body changed).
branch_id so the visit is counted at the right branch.Optional: keep the POS up to date with webhooks
If the POS shows balances or keeps its own copy of the cards, subscribe a webhook endpoint to loyalty.earned, loyalty.redeemed, loyalty.voided, loyalty.adjusted and card.reward_ready instead of looking every card up again.
data.objectis the card after the change (itsbalanceandprogress), anddata.eventthe ledger entry, with itssource:orderfor a ServeU menu order,counterfor staff on the iPad,dashboardfor an owner's correction,switchfor a program change,apifor a call with a key.- Your POS's own
/earncalls come back asloyalty.earnedtoo. Use these events only to update what you show: never call/earnbecause of one, or every visit would earn again and again. - Events can arrive out of order and cards have no
updated_at: if the exact balance matters right now, read the card withGET /api/v1/cards/{id}. card.reward_readyis the moment to tell the cashier "this customer has a free coffee".