☰ Menu · Venue rules and limits
Guides

Venue rules and limits

How a venue controls what connected registers may do: cashback levels, product categories, holds, check numbering, late checks, limits and scan checks.

Everything on this page is set by the venue owner in Developers → Register rules. The rules apply to every API key of the venue; a single key (one register, one kiosk) can have its own values for any of them. Your integration reads the rules it works under from GET /v1/account, in settings.

JSON
"settings": {
  "redemption": "hold",
  "hold_minutes": 30,
  "check_numbering": "per_register",
  "offline_max_hours": 72,
  "redeem_max_per_check": 50000,
  "redeem_max_per_day": null,
  "require_scan": true,
  "scan_window_minutes": 30,
  "allow_adjustments": false,
  "adjustment_max": 50000
}

Cashback levels

A cashback program can have levels: for example Base 5%, Silver 7% from the second visit, Gold 10% from 1000.00 spent. Levels are counted by visits, by total spent, or by whichever comes first. The owner sets them in the loyalty program. Nothing is needed on the register side: every check earns at the guest's level after that check, and every member object tells you where the guest is.

JSON
"cashback": {
  "balance": 10200,
  "held": 0,
  "available": 10200,
  "currency": "MDL",
  "earn_rate": 7,
  "level": "Silver",
  "level_basis": "any",
  "levels": [
    { "name": "Base", "earn_rate": 5, "min_visits": null, "min_spent": null },
    { "name": "Silver", "earn_rate": 7, "min_visits": 2, "min_spent": null },
    { "name": "Gold", "earn_rate": 10, "min_visits": null, "min_spent": 100000 }
  ],
  "next_level": { "name": "Gold", "earn_rate": 10, "visits_left": null, "spent_left": 80000 },
  ...
}

next_level.visits_left and next_level.spent_left are ready to show the guest ("2 more visits to Silver"). display.receipt_lines already has that line in the guest's language.

Product categories

Upload your products with their category once, then send changes. Categories appear in the owner's dashboard, where each one can be set to: not earn cashback (tobacco, alcohol), not be payable with cashback (items already on promotion), or earn its own percent instead of the guest's level (coffee 10%). The API can create products and categories and rename them, but only the owner changes these rules.

curl
curl https://loyaltyfy.io/api/v1/catalog/products/bulk \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      { "sku": "4840001", "name": "Marlboro Gold", "category": "TOB", "category_name": "Tobacco", "price": 6500 },
      { "sku": "1001", "name": "Latte", "category": "COF", "category_name": "Coffee", "price": 4500 },
      { "sku": "2040", "name": "Zeama", "category": "KIT", "category_name": "Kitchen", "price": 6500 }
    ]
  }'

# { "object": "catalog_sync", "upserted": 3 }

Then send line_items with each check. A line can carry category, or only sku: we look the category up in the catalogue. Line totals are scaled to the check amount, so discounts made on the register side are spread fairly. Lines in categories without rules earn at the guest's level.

curl
curl https://loyaltyfy.io/api/v1/transactions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "CHK-2026-000130",
    "member": "2000111703779",
    "amount": 17500,
    "line_items": [
      { "sku": "4840001", "quantity": 1, "unit_price": 6500 },
      { "sku": "1001", "quantity": 1, "unit_price": 4500 },
      { "sku": "2040", "quantity": 1, "unit_price": 6500 }
    ]
  }'

# Tobacco earns nothing, Coffee has its own 10%, Kitchen earns at the guest's level (5%):
# 0 + 450 + 325 = "cashback_earned": 775

Paying with cashback through a hold

With In the check (the default) the register sends redeem_amount with the closed check. With Through a hold the register sets the amount aside when the guest asks to pay with cashback, and the closed check confirms it. If the check never arrives (the network dropped, the sale was cancelled), the hold is released after the time the owner set, and the guest keeps the money. No discount is ever given without being taken from the balance.

  1. 1
    Hold

    POST /v1/holds with the member, the amount and the check total. The same caps as for a check apply. The response has the hold id.

  2. 2
    Capture

    POST /v1/transactions with hold_id. Leave out redeem_amount to use the whole hold, or send a smaller one.

  3. 3
    Or release

    POST /v1/holds/{id}/release when the sale is cancelled. Releasing twice is harmless.

curl
curl https://loyaltyfy.io/api/v1/holds \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hold-R3-000131" \
  -d '{ "member": "2000111703779", "amount": 3000, "check_amount": 20000, "register_id": "R3" }'

# at payment: the check captures it
curl https://loyaltyfy.io/api/v1/transactions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "000131", "register_id": "R3", "member": "2000111703779",
        "amount": 20000, "hold_id": "5b0d6c1e-8f3a-4d7e-9a61-2c4b8e0f7d19" }'
Holds count everywhere: in cashback.available of the member, in the limits, and in the venue's own cashier app, so the same money can't be spent twice.

Check numbers per register

If each register counts checks from 1, choose Each register counts its own. Then register_id is required and we store the check as <register_id>:<external_id>, for example R3:15. Use that combined id to read, void or refund the check.

Checks sent late

After an outage the register sends its queue. With a limit of, say, 72 hours, checks whose closed_at is older are refused with check_too_old, so a register restored from an old backup can't flood the venue with stale checks. A closed_at more than 10 minutes in the future is always refused.

Redeem limits

On top of the program's share of a check, the owner can set the most cashback per check and per guest per day (all registers together, by the venue's time zone). A check asking for more redeems what is allowed and the response shows the real redeem_amount; a hold asking for more is refused with limited_by. Use calculate to show the guest the exact number first.

Card must be scanned

With this rule a check, visit, hold or reward for a card is accepted only if the same key sent that card to POST /v1/scans shortly before. A card number typed by hand on the register is refused with scan_required. Send the scanner's raw output to /v1/scans, then go on as usual.

Manual adjustments

Off by default. When the owner allows it, a key with the members:adjust scope can add or take cashback with a reason, up to the limit the owner set per adjustment. Each one is in the member's history and sends balance.adjusted.

curl
curl https://loyaltyfy.io/api/v1/members/2000111703779/adjustments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "ADJ-2026-0009", "amount": 2000, "reason": "Goodwill after a complaint" }'

# a negative amount takes cashback away
Questions about an integration: api@loyaltyfy.io. We answer within one business day.