iVendNextDevelopers Request a sandbox

A rule checked before payment (a check before payment) — the recipe

A check before payment is the last word on a sale before money is taken. When the cashier presses Pay, the iVendNext POS server prices the basket itself and asks your HTTPS service: allow it, refuse it in your own words, or ask the cashier a yes/no question. Examples: members-only items, a prescription number for pharmacy items, ID above a sale amount. This recipe is for an iVendNext Partner, or the CitiXsys Consulting Team, building one.

Reference implementation: samples/s18_rule_guard/ — Members-only rule guard: it refuses a sale that holds a members-only item for a customer who is not a member, and asks the cashier for ID above the shop's limit.

When to use it. Use it for a rule that must hold when the sale is booked, whatever the till did. The till's own screens show your sentence; you ship no screen. For a rule about the moment an item is scanned (a beep, a yes/no before the line goes on), use a capture on scan instead (recipes/capture.md): that one shapes the cashier's flow; this one is enforced by the server at every door that moves money.

The four rules, here. Your answer never moves money: no price, no line, no payment. It changes nothing in the shop. It is safe to ask twice (the same basket gets the same answer). It never decides after money is taken: once a card is charged nothing is refused.

Steps

  1. Declare it in your manifest (recipes/manifest.md, guide/07_manifests.md):

    "extension_subscriptions": [{"key": "guard", "point": "check", "label": "Member rule guard",
                                 "endpoint_url": "https://your.server/rgd/check", "timeout_seconds": 3,
                                 "down_answer": "refuse"}]
    
    • label is what the cashier reads when your service is down ("Member rule guard did not answer. …").
    • down_answer: what the till does when your service is down, slow or answers what it cannot use — allow_and_flag (the default: the sale goes on and is flagged on the Sales to review report), refuse (no sale until you answer), or ask (the cashier decides). A rule that must hold even in an outage declares refuse.
    • timeout_seconds: 3 unless you raise it, 10 at most. At most three checks answer one POS Profile, ten seconds together.
    • The manifest installs it switched off. A manager's approval (may_approve) is the shop's choice, never the manifest's. The signing secret is handed back once, at install.
  2. What the shop does after install. A System Manager opens your service (Retail Extension Subscription), reads what is sent (the consent text lists every field), ticks the consent, chooses the POS Profiles it answers (none = all) and switches it on.

  3. Your service checks every call is the shop's. One POST per Pay, body {"point": "check/1", "context": {…}}, with X-iVendNext-Timestamp and X-iVendNext-Signature = base64 HMAC-SHA256 of <timestamp>.<raw body> with the secret. Check it over the raw bytes before you decide anything, and refuse a timestamp more than five minutes from yours (samples/s18_rule_guard/rule.py, verify).

  4. Decide from what you are sent. The context is the server's priced basket: each line's item code, item group, quantity in thousandths and money in whole minor units (currency_exponent says how many digits), the sale's totals, the customer and their customer group, the coupon codes, the sale details the cashier captured, and the tenders offered. Never the till's own figures, never a card.

    if not member and any(line["item_code"] in members_only for line in ctx["lines"]):
        return {"verdict": "refuse", "reason": "WINE-RESERVE is for members only. Add the member's customer to the sale first."}
    if ctx["totals"]["grand_minor"] >= round(limit * 10 ** ctx["currency_exponent"]):
        return {"verdict": "ask", "question": "This sale is 5,000.00 AED or more. Has the customer shown ID?"}
    return {"verdict": "allow"}
    

    Every sentence is plain text of 1 to 140 characters: no < or >, no control characters. One bad key or value refuses your whole answer, and your down answer applies.

  5. What the cashier sees, and what books.

    Your answer The till The booking
    allow the payment sheet opens books as always
    refuse + reason "This sale can't be paid" and your sentence; back to the basket nothing is paid: the door before any money (the card push; a gift card, store credit or cash at the booking) refuses a sale without an allowing answer for exactly this basket — "The sale changed after the check. Press Pay again."
    ask + question Yes / No; No goes back to the basket paid only after the cashier's Yes
    allow + exclude_tenders + reason those tenders drawn dimmed, "Not for this sale: …" refused for an excluded tender

    A basket changed after your answer (a line added, a price changed) is refused before money until the cashier presses Pay again, which asks you again. Once a card is charged nothing is refused: the sale books and is flagged.

  6. When your service is down or slow. The till waits your timeout, then applies your declared down answer. With refuse the cashier reads "⟨label⟩ did not answer. This sale can't be paid until it does. Try again, or get a manager."; with allow_and_flag the sale books and the round is flagged on the Extension Log. Offline, the till applies the down answer from its start-of-day snapshot and the sale is flagged when it syncs, never refused.

  7. Test it. Unit-test your rule and your service on your laptop with calls signed the way the shop signs them (samples/s18_rule_guard/tests/), and check every answer against the contract. Then press Pay on a sandbox. The money kit (recipes/money_kit.md) rings every basket through your check: an allowed sale must book exactly as without it, and a refused one must book nothing.

What the shop needs from you, and what it checks

You give The shop checks
an HTTPS address its server trusts (a real certificate) the address is consented; a private address is refused on a customer's site
a signature check on every call — (yours to do)
an answer within your timeout your declared down answer applies otherwise, and one log row is written (no body, no secret)
plain sentences of 1 to 140 characters the whole answer is judged; one bad value refuses all of it

Technical notes

What was proven where

Proven on our test bench on 2026-10-08 (iVendNext POS app at 577a8d41), run pp-20261008T124936Z-78169, 22 of 22 checks: install 3, the payment door and the failure cases 18, uninstall 1.

The money kit

The money kit rings every basket through the till's check round when a check is switched on (tools/moneykit/till.py: the check, the cashier's Yes to a question, then the card push's own gate). Its label for this sample switches S18 on for the kit's till, so every basket meets the check: an allowed or asked sale must book exactly as without it, a member's sale of the probe item books, and a walk-in's is refused and books nothing. Results: GREEN on 2026-10-08 (run pp-20261008T140521Z-29193): every one of the 69 baseline sales booked exactly as the baseline through the check round, the member's sale booked at its price, and the walk-in's was refused and booked nothing. With S18 left switched off, the same walk-in sale booked — the label went RED, as its canary must.

This page in the kit