RoxPay API
Guides

POS Terminal Local Payments (NEXO)

Accept NEXO payments via USB tethering (RNDIS) or local network on Castle terminals. Covers driver setup, network config, and 1 EUR test on S1F2L.

Local NEXO bypasses the cloud. The terminal exposes https://<terminal_ip>:8443/nexo on the same LAN or over USB tethering. Use it when you need offline POS, faster receipt, or direct control from a Raspberry Pi / small host. Cloud is the alternative via the rox-pay-vcs API when the terminal is online and assigned to your company/store.

Hardware & Prerequisites

ItemDetails
TerminalCastle S1F2 / S1F2L / S1E2L (Saturn1000F2), AMS1, P630 - tested on S1F2L-000158253425701 (0ca6:a050)
FirmwareCastles_Android 1.120.8+ (1.133.3 works), needs Local API enabled for the store
POIIDModel + serial: S1F2L-000158253425701 (with L), S1F2-000158253065912 (without). Wrong variant -> NotAllowed my POIID is S1F2L-...
HostAny Linux host with rndis_host + cdc_ether (Pi Zero 2 W, Pi4, CachyOS, Ubuntu). Arduino Uno/Nano = device-device = no link, needs Host shield.

On the Castle: Settings -> Network -> Tethering -> USB Tethering ON. The device re-enumerates:

# before
Bus 005 Device 021: ID 0ca6:a050 ... bFunctionClass 2 CDC ACM
cdc_acm 5-1.4:1.0: ttyACM0

# after
Bus 005 Device 022: ID 0ca6:a050 ... bFunctionClass 224 Wireless RNDIS
rndis_host 5-1.4:1.0 usb0: register 'rndis_host' ... 66:c3:7b:df:57:74
rndis_host 5-1.4:1.0 enp103s0f3u1u4: renamed from usb0

On CachyOS/Debian the stock driver may keep cdc_acm on the RNDIS interface and bulk OUT 0x01 then times out (USBTimeoutError 110, SERIAL_STATE 00 00). Fix with a one-time rebind:

# /usr/local/bin/castle-rndis.sh
echo "5-1.4:1.0" > /sys/bus/usb/drivers/cdc_acm/unbind 2>/dev/null || true
echo "5-1.4:1.0" > /sys/bus/usb/drivers/rndis_host/bind 2>/dev/null || true
# wait for DHCP
for i in {1..10}; do ip -4 addr show | grep -q 192.168.42. && break; sleep 1; done

Udev to make it permanent:

# /etc/udev/rules.d/99-castle.rules
ACTION=="add", ATTR{idVendor}=="0ca6", ATTR{idProduct}=="a050", RUN+="/usr/local/bin/castle-rndis.sh"

Result on host:

ip -4 addr show enp103s0f3u1u4
# enp103s0f3u1u4: inet 192.168.42.121/24 brd 192.168.42.255
ip route show
# default via 192.168.42.129 dev enp103s0f3u1u4 proto dhcp
ping -c 2 192.168.42.129 # 0.5ms
# terminal is the gateway
ip neigh show dev enp103s0f3u1u4
# 192.168.42.129 lladdr ca:39:22:dd:ff:ad REACHABLE

192.168.42.129 is always the Castle, host gets 192.168.42.x via DHCP. No manual dhclient needed with NetworkManager.

Option B - WiFi / Local LAN

Connect the Castle to the same WiFi as the host: Settings -> Network -> WiFi -> <SSID>. Note IP in Settings -> Device Info -> IP (e.g. 192.168.1.205 or 192.168.1.40). Use that IP instead of 192.168.42.129 below. Ensure Local API is enabled for that store in the provider dashboard.

Cloud alternative via rox-pay-vcs: create a store order and use the POS endpoints. The backend forwards to the terminal via cloud when it is online and assigned to your company/store. No direct terminal IP needed.

# 1. Create a store order (example)
curl -X POST https://app.roxpay.eu/swiftline/cash-register/orders \
 -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
 -d '{"store_id": 123, "items": [{"id": 1, "quantity": 1}]}'

# 2. Prepare POS payment for that order (cloud)
curl -X POST https://app.roxpay.eu/swiftline/cash-register/orders/{order_id}/pay/pos \
 -H "Authorization: Bearer YOUR_TOKEN"

# 3. Complete after terminal response
curl -X POST https://app.roxpay.eu/swiftline/cash-register/orders/{order_id}/pay/pos/complete \
 -H "Authorization: Bearer YOUR_TOKEN" -d '{"rbc_id": "TRX-..."}'

Check assignment in back-office: GET /swiftline/cash-register/terminals or /swiftline/cash-register/terminal (from POS cookie). If your S/N not listed -> assign it first (POST /swiftline/cash-register/terminals/assign with terminal_id = POIID and store_id).

Local NEXO API

  • Endpoint: https://<terminal_ip>:8443/nexo (GET -> 404, POST only)
  • TLS: Self-signed terminal certificate (test fleet). Use curl -k or requests verify=False / WiFiClientSecure::setInsecure().
  • Headers: Content-Type: application/json only (local needs no API key).
  • POIID must match exactly including L: S1F2L-... vs S1F2-....
  • ProtocolVersion: 3.0, MessageClass Service, MessageCategory Payment or Print.

Payment - 1 EUR Test

Validated on S1F2L-000158253425701 via RNDIS (enp... 192.168.42.121 -> 192.168.42.129):

curl -k --request POST --url 'https://192.168.42.129:8443/nexo' \
 --header 'Content-Type: application/json' --data '{
  "SaleToPOIRequest": {
    "MessageHeader": {
      "ProtocolVersion": "3.0",
      "MessageClass": "Service",
      "MessageCategory": "Payment",
      "MessageType": "Request",
      "SaleID": "USBTEST-001",
      "ServiceID": "000003",
      "POIID": "S1F2L-000158253425701"
    },
    "PaymentRequest": {
      "SaleData": {
        "SaleTransactionID": {
          "TransactionID": "USBTEST-001",
          "TimeStamp": "2026-08-31T12:10:00Z"
        },
        "SaleToAcquirerData": "shopperReference=usb-test&recurringProcessingModel=UnscheduledCardOnFile&authorisationType=Final&manualCapture=false"
      },
      "PaymentTransaction": {
        "AmountsReq": {
          "Currency": "EUR",
          "RequestedAmount": 1.00
        }
      }
    }
  }
}'

Success response (Result Success):

{
  "SaleToPOIResponse": {
    "MessageHeader": {"POIID":"S1F2L-000158253425701","SaleID":"USBTEST-001","ServiceID":"000003"},
    "PaymentResponse": {
      "PaymentResult": {
        "AmountsResp": {"AuthorizedAmount":1,"Currency":"EUR"},
        "OnlineFlag": true,
        "PaymentAcquirerData": {"AcquirerPOIID":"S1F2L-000158253425701","ApprovalCode":"123456"},
        "PaymentInstrumentData": {"CardData":{"MaskedPan":"411111 **** 0001","PaymentBrand":"visa","EntryMode":["Contactless"]}}
      },
      "Response": {
        "Result": "Success",
        "AdditionalResponse": "pspReference=C4PPDMXVT8VQ3M75&authorisedAmountValue=100&posEntryMode=CLESS_CHIP&tid=53425701&refusalReasonRaw=APPROVED&store=STR-20260826-DPV9H25UD7&..."
      }
    }
  }
}
  • AuthorizedAmount 1 = €1.00, MaskedPan 411111 is test card. On live with real card it is a real capture.
  • Wrong POIID S1F2-... -> {"Result":"Failure","ErrorCondition":"NotAllowed","AdditionalResponse":"message=MessageHeader.POIID: NotAllowed Value: S1F2-000158253425701, Reason: my POIID is S1F2L-000158253425701"}

Generate fresh IDs per request: SaleID = TransactionID, ServiceID = random 1_000_000_000..9_999_999_999, TimeStamp = UTC YYYY-MM-DDTHH:MM:SSZ.

curl -k --request POST --url 'https://192.168.42.129:8443/nexo' \
 --header 'Content-Type: application/json' --data '{
  "SaleToPOIRequest": {
    "MessageHeader": {"ProtocolVersion":"3.0","MessageClass":"Device","MessageCategory":"Print","MessageType":"Request","ServiceID":"PRINT01","SaleID":"POSPRINT-001","POIID":"S1F2L-000158253425701"},
    "PrintRequest": {
      "PrintOutput": {
        "DocumentQualifier": "Document",
        "ResponseMode": "PrintEnd",
        "OutputContent": {
          "OutputFormat": "Text",
          "OutputText": [
            {"Text":"LODGE EASY","CharacterStyle":"Bold","Alignment":"Centred","EndOfLineFlag":true},
            {"Text":"Test print via USB tethering","Alignment":"Centred","EndOfLineFlag":true},
            {"Text":"Amount: EUR 1.00","Alignment":"Left","EndOfLineFlag":true},
            {"Text":"Thank you!","Alignment":"Centred","EndOfLineFlag":true}
          ]
        }
      }
    }
  }
}'
# -> {"PrintResponse":{"Response":{"Result":"Success"}}}

Small Hardware: Raspberry Pi

Pi Zero 2 W or Pi4 as host is enough. Castle USB-C -> Pi USB-A (Pi4) or OTG C->A (Zero). Keep tethering ON.

# /home/pi/castle_pay.py - pip install requests
import requests, json, datetime, random, urllib.parse
POIID="S1F2L-000158253425701"
URL="https://192.168.42.129:8443/nexo"
def pay(amount_eur=1.00):
    txn=f"PI-{int(datetime.datetime.now().timestamp())}"
    sid=str(random.randint(1000000000,9999999999))
    ts=datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    payload={
      "SaleToPOIRequest":{
        "MessageHeader":{"ProtocolVersion":"3.0","MessageClass":"Service","MessageCategory":"Payment","MessageType":"Request","SaleID":txn,"ServiceID":sid,"POIID":POIID},
        "PaymentRequest":{
          "SaleData":{"SaleTransactionID":{"TransactionID":txn,"TimeStamp":ts},"SaleToAcquirerData":urllib.parse.urlencode({"shopperReference":"pi","recurringProcessingModel":"UnscheduledCardOnFile","authorisationType":"Final","manualCapture":"false"})},
          "PaymentTransaction":{"AmountsReq":{"Currency":"EUR","RequestedAmount":float(amount_eur)}}
        }
      }
    }
    r=requests.post(URL, json=payload, verify=False, timeout=30)
    j=r.json()
    print(json.dumps(j, indent=2))
    return j["SaleToPOIResponse"]["PaymentResponse"]["Response"]["Result"]=="Success"

if __name__=="__main__":
    pay(1.00)

For Arduino/ESP32: needs Host + RNDIS stack (e.g. ESP32-S3 TinyUSB host). Easier: use ESP32 WiFi to 192.168.42.129 or to the terminal's WiFi IP and same POST with WiFiClientSecure::setInsecure() + HTTPClient. Uno without Host shield cannot do USB.

Troubleshooting

SymptomCauseFix
NotAllowed my POIID is S1F2L-...Used S1F2- without LUse exact lsusb serial S1F2L-000158253425701
USBTimeoutError 110 bulk OUTHost still on cdc_acm not rndis_hostRun castle-rndis.sh rebind as above, check ls -l /sys/bus/usb/devices/5-1.4:1.0/driver -> rndis_host and enp... appears
No enp... / usb0rndis_host not loadedmodprobe rndis_host cdc_ether, re-plug, check dmesg rndis_host ... register
404 on GET /nexoGET not allowedUse POST only
Configurable USB Mode greyedManaged by provider, boardedChange via provider dashboard -> Terminal Settings, or reassign to Inventory first
Ping 192.168.42.129 failsTethering off or wrong driverSettings -> Tethering ON, rebind as above, ip addr should show 192.168.42.x
Cloud POS not finding terminalNot assigned to your company/storePOST /swiftline/cash-register/terminals/assign with terminal_id = POIID and store_id

Cloud vs Local

Local (192.168.42.129:8443 / 192.168.1.x:8443)Cloud (rox-pay-vcs pay/pos)
Needs internetNo (offline works)Yes, terminal must be online and assigned
Needs assignmentNoYes (/terminals/assign)
AuthNone (-k)Authorization: Bearer YOUR_TOKEN (rox-pay-vcs)
Latency~6s for 1 EUR~90s timeout handled by backend

Keep USB Tethering ON for Pi. For pure WiFi, use the WiFi IP instead of 192.168.42.129 - same payload.

On this page