iVendNextDevelopers Request a sandbox

A value captured at the scan, and a code before a new customer (capture) — the recipe

A capture asks your HTTPS service at a moment the cashier did not choose, at one of two events:

This recipe is for an iVendNext Partner, or the CitiXsys Consulting Team, building one.

Reference implementation: samples/s20_capture/ — Warranty capture and a member code: a warranty reference on every handset line, and a code by text message before a member is added.

When to use it. On scan you shape the cashier's flow; you are not a booking gate (for a rule that must hold when the sale is booked, use a check before payment, recipes/rule_guard.md). On customer add the server is the gate on the till's own doors: no customer is created there until it has recorded your answer for that cashier, that name and that phone. Two exceptions, both flagged on the Sales to review report and never refused, because the customer has already paid: a customer added offline is created when the sale syncs, and the receipt's add a contact after a walk-in sale (one customer per paid sale) is created too. A customer made in iVendNext Desk or through another API is outside the gate. A rule that must never let a barred number in needs your own reconciliation as well.

The four rules, here. No answer writes a customer field, and none moves money: no price, total or tender is sent, and nothing you answer changes one. A value you put on a line is annotation. A failed capture never fails the scan.

Steps

  1. Declare one service per event in your manifest, each at its own address: the address then tells your service which secret to check the signature with, before it reads the body.

    "extension_subscriptions": [
      {"key": "on_scan", "point": "capture", "label": "Warranty capture", "endpoint_url": "https://your.server/cap/scan",
       "timeout_seconds": 1.5, "down_answer": "allow_and_flag", "settings": {"event": "scan", "item_groups": ["iPhone", "iPad"]}},
      {"key": "on_customer", "point": "capture", "label": "Member code", "endpoint_url": "https://your.server/cap/customer",
       "timeout_seconds": 3, "down_answer": "refuse", "settings": {"event": "customer_add"}}]
    
    • On scan: item_groups (at most 50; a group names every group below it) and/or item_field (an Item field that is set); 1.5 seconds unless raised, 3 at most.
    • On customer add: 3 seconds unless raised, 10 at most.
    • down_answer: allow_and_flag (the default: the scan goes on, or the customer may be created, and the log row is flagged) or refuse.
    • Both install switched off; each signing secret is handed back once.
  2. What the shop does after install. A System Manager reads what each service is sent, ticks the consent, chooses the POS Profiles and switches it on. For a value on the line, the shop keeps an active line attribute (Transaction Item Attribute) and tells you its name.

  3. Check every call is the shop's — the same signature as every extension point (base64 HMAC-SHA256 of <timestamp>.<raw body>), with each subscription's own secret, over the raw bytes, before you parse anything (samples/s20_capture/service/app.py).

  4. On scan: answer from the scan's own values. You receive the line's item code, item group, quantity in thousandths, unit, batch, serial and the code scanned — and no line id: the line does not exist until your answer lets it on. Two units of one model scanned by their item barcode look the same, so S20 makes a new reference for every scan. A value for the line:

    {"verdict": "allow", "line_attributes": [{"name": "Warranty ref", "value": "WR-3F9A0C21B7"}]}
    

    The cashier sees it on a confirm sheet first; on Add with these details it lands on that line, which is then its own line (a later scan of the same item never merges into it).

  5. On customer add: refuse, allow or challenge.

    {"verdict": "challenge", "prompt": "Ask the customer for the 6-digit code we just sent.", "length": 6}
    

    Keep the code against context.challenge_id and send it to the customer through your own gateway (S20's sample writes it to a file; your service calls its SMS or WhatsApp provider). The till sends what the customer reads out as {"event": "verify", "challenge_id": …, "code": …}; you answer {"verdict": "ok"} or {"verdict": "refuse", "reason": "That code is not right."}. The product counts the tries: three wrong codes end the check, and a challenge lives ten minutes. Keep your codes for at most ten minutes too.

  6. What the cashier sees.

    Your answer The till
    scan · allow with values a confirm sheet, then the item with your value on its line
    scan · confirm + question a beep and the question; No leaves the item off
    scan · refuse + reason "This item can't be added" and your sentence
    customer · refuse + reason "This customer can't be added" and your sentence
    customer · challenge a code field with your prompt; a wrong code: your reason and "⟨n⟩ tries left."
    a create without your recorded answer refused by the server: "Check the customer's code first." — the till then asks you
  7. When your service is down or slow. On scan, allow_and_flag lets the line on and flags it; refuse keeps the item off. On customer add, refuse reads "⟨label⟩ did not answer. The customer can't be added until it does." The scan never fails because of you: a capture that cannot run applies your declared answer.

  8. Test it. Unit-test the core and the service with calls signed the way the shop signs them (samples/s20_capture/tests/), and check every answer against the contract. Then scan and add a customer on a sandbox.

Technical notes

What was proven where

Proven on our test bench on 2026-10-08 (iVendNext POS app at 577a8d41), run pp-20261008T125051Z-84203, 16 of 16 checks, over HTTPS, through the till's own calls as the cashier (api.mpos_cart.scan, api.capture.customer_add, api.capture.verify, api.customer.quick_create):

Case Result
Install two services switched off, one per event; each secret handed back once
A handset of the named groups scanned the service's reference lands as a value of the shop's attribute ({"transaction_item_attribute": "SET5 Warranty ref", "attribute_value": "WR-CED7D457F2"}) — the product hands it to the till in that shape
An item outside the groups nobody is called, no log row
The service down · the capture itself failing the scan still finds the item and the line goes on, flagged (allow_and_flag)
A new customer without a phone · with a barred phone refused in the service's words
A new phone challenged: "Ask the customer for the 6-digit code we just sent.", a code sent to that phone
A wrong code refused, two tries left
The create before the right code refused by the server: "Check the customer's code first." — no customer
The right code ok, with the create's token
That token offered for another name refused — no customer of that name
The create with the token the customer is created, name and phone as typed
The same phone again challenged again (no phone skips the code)
Three wrong codes the check ends; even the right code is then refused ("This code check has ended. Add the customer again.")

The captured reference on a booked sale: the money kit's label for this sample. Results: GREEN on 2026-10-08 (run pp-20261008T125956Z-562): the booked line carries the service's reference; every other sale as the baseline. With S20 left off, the line carries none and the label goes RED.

Not run here: a customer added offline (created at sync, flagged) and the receipt's contact after payment; a confirm or refuse on scan; the camera's pause.

This page in the kit