StoreRailStoreRail Docs
DocsStoreRail API Gateway

StoreRail API Gateway

API v1 ⚠ Early access · validated against a POS simulator only

The gateway runs on a PC in the restaurant. Your kiosk sends it an order; it records the order, stages it on NCR Aloha, waits for your app to take payment, then releases it to the kitchen. Your app never talks to the POS.

Connecting

Kiosks reach the gateway at https://<gateway host>:8443 on the store LAN.

  • TLS trust. Trust only the store’s internal CA root. Pilot stores may pin the certificate’s public key instead.
  • Strict JSON. camelCase, case-sensitive. Unknown or duplicate properties return VALIDATION_FAILED.
  • Money is a decimal string in major units, for example "43.50". Never a JSON number.
  • Correlation. Every response carries X-Correlation-Id. Log it with every error.
ℹ Health checks are unauthenticated. Use GET /health/ready to show an “ordering unavailable” screen: it returns 200 when ready and 503 when not.

Signing requests

Every /v1/orders request is signed with RFC 9421 HTTP Message Signatures, using the device’s hardware-backed P-256 key. The device presents its licence in X-Device-License. The gateway — not the app — decides whether a device may order.

HeaderValue
X-Device-LicenseCompact JWS licence issued for this device and store
Content-DigestSHA-256 of the exact body bytes you send
Signature-InputCovered components, created, a fresh nonce, keyid, alg="ecdsa-p256-sha256"
SignatureES256 signature over the signature base
  • created must be within 300 seconds of the gateway clock.
  • A nonce is accepted once. Generate a new one for every request, including retries.
  • Sign the bytes you send. Don’t re-serialize the JSON after computing the digest.

Order and payment flow

The gateway stages the order on the POS without sending it to the kitchen. Your app charges the POS-computed total, then releases the order.

  1. 1
    SubmitPOST /v1/orders with a new Idempotency-Key. Expect awaitingPayment with a total, or pending — then poll.
  2. 2
    ChargeThrough your payment provider, for exactly total.amount, and only while the order is awaitingPayment.
  3. 3
    ReleasePOST /v1/orders/{orderId}/release. The gateway records the tender on the check first, then sends it to the kitchen.
  4. or
    CancelPOST /v1/orders/{orderId}/cancel if the guest leaves before paying. Unreleased orders expire after 10 minutes by default.
⚠ Never send card data. paymentReference is your provider’s transaction reference, 1–40 printable characters. Values shaped like a card number are rejected.
POST

/v1/orders

Submit order intent. The reply waits up to 3 seconds for staging. Each order gets its own POS check.

HTTP · request
POST /v1/ordersIdempotency-Key: 6f1c2b0e-4a1d-4f7e-9b0a-1c2d3e4f5a6bContent-Type: application/jsonX-Device-License: <licence>Content-Digest: sha-256=:<digest>:Signature-Input: sig1=(…)Signature: sig1=:<signature>:{"orderType":"takeAway","items":[  {"posItemId":"1001","quantity":2,   "modifiers":[{"posItemId":"5003","action":"without"}]},  {"posItemId":"2040","quantity":1}]}
201 Created · JSON
{  "orderId": "01926f5e-8a1c-7b3e-9c2d-4f5a6b7c8d9e",  "status": "awaitingPayment",  "orderType": "takeAway",  "total": { "amount": "30.00", "currency": "SAR" },  "awaitingPaymentExpiresAt": "2026-09-25T08:10:00+00:00",  "createdAt": "2026-09-25T08:00:00+00:00"}

Example values are fictional. Cart limits: quantity 1–99, up to 20 modifiers per line, notes up to 40 characters, and at most 40 POS entries per cart.

POST

/v1/orders/{orderId}/release

Record the payment and send the order to the kitchen. The body accepts exactly three properties.

JSON · body
{"tenderType":"card","amount":"30.00","paymentReference":"TXN-8F3K2Q9D1Z"}

A release retried with identical details returns the current order and charges nothing again. Different details return IDEMPOTENCY_CONFLICT.

Order status

Read with GET /v1/orders/{orderId}. The response reflects the gateway ledger, never raw POS errors.

statusWhat your app does
pendingPoll. Do not charge.
awaitingPaymentCharge total, then release. Or cancel.
releasingPoll. Do not charge again.
reconcilingPoll. Do not charge, resubmit or reverse.
completedDone. Show the order as confirmed.
failedDo not charge. If a payment was taken, reverse it.
cancelledIf a payment was taken, reverse it.
⚠ Reconciling is a real status. It means the POS outcome is unknown. Don’t charge, resubmit or reverse — poll about once a second until it resolves. If attention is set, tell the guest staff will help.

Retries and idempotency

  • Use an HTTP timeout of at least 10 seconds.
  • Retry a submit with the same key and same body. A recorded order returns 200 with Idempotency-Replay: true.
  • Never reuse a key for a different order — that returns IDEMPOTENCY_CONFLICT.
  • Every retry is a new signed request with a fresh nonce.

Errors

Every error is an RFC 9457 Problem Details body (application/problem+json). Branch on code, never on title. Always log correlationId.

HTTPcodeWhat to do
400VALIDATION_FAILEDFix the request. The idempotency key is not consumed.
401DEVICE_NOT_AUTHENTICATEDCheck the kiosk clock and signing code.
403LICENSE_INVALIDThe operator must issue a new licence.
403LICENSE_EXPIREDThe operator must renew the licence.
403DEVICE_NOT_AUTHORIZEDLicense this device for this store.
404ORDER_NOT_FOUNDNo order with this id for this device.
409IDEMPOTENCY_CONFLICTUse a new key for a new order.
409PAYMENT_AMOUNT_MISMATCHNothing released. Charge exactly the total.
409ORDER_STATE_INVALIDIf you already charged, reverse the payment.
409ORDER_RECONCILINGDo not reverse or release again. Poll.
503POS_UNAVAILABLENothing recorded. Retry later with the same key.
500INTERNAL_ERRORRetry safely; report the correlationId.

Health

GET /healthProcess liveness. Unauthenticated.
GET /health/readyReady to accept orders: the POS preflight has passed. 200 or 503.
Planning an integration?Get the full OpenAPI contract and lab access.
Request access