RRapidHub Developers
RapidHub for POS vendors

Connect once. Receive every delivery platform.

RapidHub is an order hub. Your POS integrates with one API and receives orders from Wolt, Uber Eats, Delivery Hero (foodora, Glovo …), Just Eat Takeaway and the built-in Mock sandbox platform, all in one canonical JSON format. State changes, menus and store hours flow back to every platform through the same API.

REST + SignalR (WebSocket)JSON, camelCaseMoney in minor unitsOrder schema 2.0
Platforms Wolt Uber Eats Delivery Hero Just Eat Takeaway Mock (sandbox) webhooks RapidHub verify · fetch · normalize canonical orderone JSON for every platform outbox · retries · reconcile Your POSone integration OrderEvent (SignalR) REST: state, menu, store hours
Platforms talk to RapidHub; your POS only talks to RapidHub.

Key concepts

ConceptWhat it is
TenantYour customer's account in RapidHub (the merchant or chain). Platform connections belong to the tenant.
RestaurantOne branch / till location of the tenant. Orders are routed to exactly one restaurant.
Restaurant keyThe machine credential of a restaurant. Your POS uses it for every REST call and the real-time connection.
ConnectionOne store on one platform (e.g. a Wolt venue), linked to a restaurant. It has an id, a platform, a platformStoreId and capabilities.
Canonical orderThe platform-independent order JSON your POS receives (schema 2.0).
OrderEventA real-time message that carries a full canonical order: Received, Updated or Canceled.

Quickstart

This walk-through uses the built-in Mock platform, so you need no Wolt, Uber Eats or Delivery Hero test store.

  1. Get a RapidHub accountSign up at hub.rapidpaygroup.com/signup (or ask RapidHub to create a tenant for your customer).
  2. Create a restaurant and copy its keyDashboard → Restaurants → create a restaurant. The restaurant key is shown once; store it in your POS settings. You can rotate it later; the old key stops working immediately.
  3. Create a Mock connectionDashboard → Sandbox → pick the restaurant → Create Mock connection. No credentials are needed.
  4. Connect your POSOpen the real-time connection to /hubs/orders and call GET /pos/orders/active once to catch up.
  5. Push a menuSend your menu with POST /pos/catalog/push. The Mock platform validates it like a real platform.
  6. Send a test orderDashboard → Sandbox → Send test order. Your POS receives an OrderEvent of kind Received.
  7. Drive the orderAccept, mark ready and deliver it with POST /pos/orders/{id}/state. Each call is listed under Sandbox → Test orders.

Check the key with your first request:

curl https://hub.rapidpaygroup.com/pos/orders/active \
  -H "X-Restaurant-Key: $RAPIDHUB_KEY"
using System.Text.Json;

using var http = new HttpClient { BaseAddress = new Uri("https://hub.rapidpaygroup.com") };
http.DefaultRequestHeaders.Add("X-Restaurant-Key", restaurantKey);

var json = await http.GetStringAsync("/pos/orders/active");   // 401 = wrong or inactive key
using var doc = JsonDocument.Parse(json);
foreach (var order in doc.RootElement.EnumerateArray())
    Console.WriteLine($"{order.GetProperty("displayCode")} {order.GetProperty("state")}");
// Ktor client + kotlinx.serialization
val http = HttpClient(CIO) {
    install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true; explicitNulls = false }) }
    defaultRequest {
        url("https://hub.rapidpaygroup.com/")
        header("X-Restaurant-Key", restaurantKey)
    }
}

val res = http.get("pos/orders/active")
check(res.status != HttpStatusCode.Unauthorized) { "Invalid restaurant key" }
val orders = Json.parseToJsonElement(res.bodyAsText()).jsonArray
const res = await fetch("https://hub.rapidpaygroup.com/pos/orders/active", {
  headers: { "X-Restaurant-Key": process.env.RAPIDHUB_KEY },
});
if (res.status === 401) throw new Error("Invalid restaurant key");
const orders = await res.json(); // array of canonical orders

Authentication

Every POS request is authenticated with the restaurant key. One key identifies one restaurant; it only sees that restaurant's orders.

WhereHow to send the key
REST /pos/*X-Restaurant-Key: <key> (preferred) or Authorization: Bearer <key>. If both are sent, X-Restaurant-Key wins.
SignalR /hubs/ordersQuery string ?access_token=<key> or header X-Restaurant-Key. The hub does not read Authorization: Bearer.
  • A key is 48 lowercase hex characters. RapidHub stores only a hash, so it cannot show the key again. Rotating it (dashboard → Restaurants) invalidates the old key at once.
  • A missing, unknown or deactivated key returns 401 with an empty body. On the hub, the connection is closed right after the handshake.
  • Every authenticated call updates the restaurant's "last seen" time in the dashboard.
  • All traffic is HTTPS / WSS. Treat the key like a password: keep it on the till or your server, never in a customer-facing web page or app.
ⓘ Catalog scope

Catalog and store endpoints work on the platform connections of the tenant that owns the key. A chain with several restaurants under one tenant sees all of the tenant's connections in GET /pos/catalog/connections. Always pass explicit connectionIds for the restaurant you are configuring.

Base URL & environments

REST base URL
https://hub.rapidpaygroup.com
Real-time hub
wss://hub.rapidpaygroup.com/hubs/orders
OpenAPI
/integration/openapi.json

There is one RapidHub host. Whether an order is a test or a live order depends on the connection, not on the URL:

  • Mock connections (platform: "Mock") are RapidHub's sandbox. They never call a real platform.
  • Real platform connections are either Staging (the platform's test environment, e.g. a Wolt test venue) or Production. Every order carries environment.

Real-time events

RapidHub pushes orders to your POS over an ASP.NET Core SignalR hub. Any SignalR client works, and so does a plain WebSocket (see below).

WSS/hubs/orders?access_token=<restaurant key> or X-Restaurant-Key
  • Each connection joins its restaurant's group. Every device that connects with the same key receives every event of that restaurant.
  • The server sends a single client method, OrderEvent, with one argument: the envelope below.
  • The hub has no methods for you to call. State changes go through REST.

The OrderEvent envelope

FieldTypeDescription
eventKindstringReceived, Updated or Canceled. See event kinds.
platformstringPlatform of the order, e.g. Wolt, Mock.
platformOrderIdstringThe platform's order id.
orderIduuidRapidHub order id. Same as order.id; use it for state calls and de-duplication.
orderstringThe full canonical order as a JSON string. Parse it a second time.
JSONOrderEvent argument
{
  "eventKind": "Received",
  "platform": "Mock",
  "platformOrderId": "mock-261015123000-1",
  "orderId": "3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40",
  "order": "{\"schemaVersion\":\"2.0\",\"id\":\"3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40\",\"platform\":\"Mock\", … }"
}

Connect

// dotnet add package Microsoft.AspNetCore.SignalR.Client
using System.Text.Json;
using Microsoft.AspNetCore.SignalR.Client;

var hub = new HubConnectionBuilder()
    .WithUrl("https://hub.rapidpaygroup.com/hubs/orders", o =>
    {
        // The hub reads X-Restaurant-Key or ?access_token= (not "Authorization: Bearer").
        o.Headers["X-Restaurant-Key"] = restaurantKey;
    })
    .WithAutomaticReconnect()
    .Build();

hub.On<OrderEvent>("OrderEvent", async e =>
{
    using var order = JsonDocument.Parse(e.Order);              // e.Order is a JSON string
    await store.UpsertAsync(e.OrderId, e.EventKind, order.RootElement); // idempotent, keyed by OrderId
});

hub.Reconnected += _ => CatchUpAsync();                          // GET /pos/orders/active
hub.Closed += async _ => { await Task.Delay(5000); await hub.StartAsync(); await CatchUpAsync(); };

await hub.StartAsync();
await CatchUpAsync();

public sealed record OrderEvent(string EventKind, string Platform, string PlatformOrderId, Guid OrderId, string Order);
// npm i @microsoft/signalr   (Node.js, Electron, or a POS web app running on the till)
import * as signalR from "@microsoft/signalr";

const key = process.env.RAPIDHUB_KEY;
const hub = new signalR.HubConnectionBuilder()
  .withUrl(`https://hub.rapidpaygroup.com/hubs/orders?access_token=${encodeURIComponent(key)}`, {
    transport: signalR.HttpTransportType.WebSockets,
    skipNegotiation: true,
  })
  .withAutomaticReconnect()
  .build();

hub.on("OrderEvent", (e) => {
  const order = JSON.parse(e.order);          // canonical order (JSON string → object)
  upsertOrder(e.orderId, e.eventKind, order); // idempotent, keyed by e.orderId
});
hub.onreconnected(() => catchUp());           // GET /pos/orders/active
hub.onclose(() => setTimeout(() => hub.start().then(catchUp), 5000));

await hub.start();
await catchUp();
// Plain Ktor WebSocket, no SignalR library (works on every Kotlin Multiplatform target).
private const val RS = '\u001E'                       // SignalR record separator
private val json = Json { ignoreUnknownKeys = true }

suspend fun listen(http: HttpClient, key: String, onEvent: suspend (OrderEventEnvelope) -> Unit) {
    http.webSocket("wss://hub.rapidpaygroup.com/hubs/orders?access_token=$key") {
        send("""{"protocol":"json","version":1}$RS""")             // 1. handshake
        val reply = (incoming.receive() as Frame.Text).readText()    // "{}" + RS = accepted
        require(!reply.contains("\"error\"")) { "handshake rejected: $reply" }

        val pinger = launch { while (isActive) { delay(15_000); send("""{"type":6}$RS""") } }
        try {
            var buffer = ""
            for (frame in incoming) {
                buffer += (frame as? Frame.Text)?.readText() ?: continue
                while (true) {
                    val end = buffer.indexOf(RS).takeIf { it >= 0 } ?: break
                    val raw = buffer.substring(0, end)
                    buffer = buffer.substring(end + 1)
                    if (raw.isBlank()) continue
                    val msg = json.parseToJsonElement(raw).jsonObject
                    when (msg["type"]?.jsonPrimitive?.int) {
                        1 -> if (msg["target"]?.jsonPrimitive?.content == "OrderEvent")   // invocation
                            onEvent(json.decodeFromJsonElement(msg["arguments"]!!.jsonArray[0]))
                        7 -> return@webSocket                                             // close
                        else -> Unit                                                      // 6 = ping
                    }
                }
            }
        } finally {
            pinger.cancel()
        }
    }
}

@Serializable
data class OrderEventEnvelope(
    val eventKind: String,
    val platform: String,
    val platformOrderId: String,
    val orderId: String,
    val order: String,            // canonical order as a JSON string
)
✓ Always catch up after connecting

Call GET /pos/orders/active after every successful (re)connect. Events sent while your POS was offline are not replayed on the socket; see delivery guarantees.

Without a SignalR library

SignalR's JSON protocol is easy to speak over a raw WebSocket. This is how RapidPay's own Kotlin Multiplatform POS client connects.

  1. Open wss://hub.rapidpaygroup.com/hubs/orders?access_token=<restaurant key>. You can skip the HTTP negotiate step; the server accepts a direct WebSocket.
  2. Handshake: send {"protocol":"json","version":1} followed by the record separator 0x1E. The server answers {}+0x1E, or {"error":"…"}+0x1E.
  3. Messages are JSON objects, each terminated by 0x1E. One WebSocket frame can contain several messages, and one message can span frames: buffer and split on 0x1E.
  4. Types: 1 = invocation (target: "OrderEvent", arguments: [envelope]), 6 = ping, 7 = close (optional error). Ignore other types.
  5. Keep-alive: send {"type":6}+0x1E every 15 seconds. The server also pings every 15 seconds and drops clients it has not heard from for 30 seconds.
  6. Reconnect with backoff, for example min(30 s, 2^attempt s), reset after the connection was stable for a few seconds, then catch up.
TextFrames on the wire (␞ = 0x1E)
→ {"protocol":"json","version":1}␞
← {}␞
← {"type":6}␞
← {"type":1,"target":"OrderEvent","arguments":[{"eventKind":"Received","platform":"Mock","platformOrderId":"mock-261015123000-1","orderId":"3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40","order":"{\"schemaVersion\":\"2.0\",\"id\":\"3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40\", …}"}]}␞
→ {"type":6}␞

Event kinds

Every event carries the complete, current order. Replace your stored copy; never apply events as diffs.

eventKindWhenWhat to do
ReceivedRapidHub stored a new order for your restaurant. Usually state: "Received"; already Accepted when the platform accepted it itself (acceptance: "AutoByPlatform").Show it, print a ticket, start the accept timer (acceptDeadline).
UpdatedAnything changed: your own state change (yes, you get an echo), a platform status change (courier picked up, delivered), a customer edit, an auto-accept.Replace the order. Do not re-print unless items changed.
CanceledThe platform cancelled or rejected the order (customer cancel, accept deadline missed, platform auto-reject). state is Canceled or Rejected, rejectReason may say why.Stop preparing, alert the staff, void any open receipt.

Delivery guarantees

RapidHub writes every order change to a transactional outbox in the same database transaction as the order itself, then delivers it to your restaurant's SignalR group.

  • At-least-once. The same event can arrive more than once (several devices, retries, reconnects). De-duplicate by orderId and treat every event as "this is the order now".
  • Offline restaurant. If no device of the restaurant is connected, delivery is retried with exponential backoff (2, 4, 8 … 256 seconds, 8 attempts, about 8½ minutes) and then parked. Parked events are not pushed again automatically, so poll GET /pos/orders/active after every reconnect, and every 30–60 seconds while the socket is down.
  • Partially online. When at least one device is connected the event counts as delivered. A second till that was offline at that moment must catch up through the REST call too.
  • Ordering. Events are not sequence-numbered. Never move an order backwards: keep the most advanced state and let Canceled / Rejected from the platform win, the same merge rule RapidHub uses.
  • Missed platform webhooks. RapidHub reconciles active orders with each platform every 2 minutes, so an order whose webhook was lost still reaches you, as Received or Updated.
  • Same id forever. A re-fetched or updated platform order keeps its RapidHub id.
⚠ Orders that disappear from the active list

If an order you hold as open is missing from GET /pos/orders/active, it reached a terminal state (Delivered, Rejected or Canceled) while you were offline. RapidHub has no endpoint to read a single finished order yet. Mark the order "closed on the platform" and ask the staff to check it, rather than guessing whether it was delivered or cancelled.

Active orders (REST)

GET/pos/orders/activeX-Restaurant-Key

Returns every order of the restaurant that is not in a terminal state, oldest first, as a plain JSON array of canonical orders. There is no paging; the list holds what is currently open.

curl https://hub.rapidpaygroup.com/pos/orders/active \
  -H "X-Restaurant-Key: $RAPIDHUB_KEY"
async Task CatchUpAsync()
{
    var json = await http.GetStringAsync("/pos/orders/active");
    using var doc = JsonDocument.Parse(json);
    var open = new HashSet<Guid>();
    foreach (var order in doc.RootElement.EnumerateArray())
    {
        var id = order.GetProperty("id").GetGuid();
        open.Add(id);
        await store.UpsertAsync(id, "Updated", order);
    }
    await store.MarkClosedOnPlatformAsync(exceptIds: open);   // see the warning above
}
suspend fun catchUp() {
    val body = http.get("pos/orders/active").bodyAsText()
    val orders = json.parseToJsonElement(body).jsonArray
    val open = orders.map { it.jsonObject["id"]!!.jsonPrimitive.content }.toSet()
    orders.forEach { store.upsert(it.jsonObject) }
    store.markClosedOnPlatform(except = open)
}
async function catchUp() {
  const res = await fetch(`${BASE}/pos/orders/active`, { headers: { "X-Restaurant-Key": key } });
  const orders = await res.json();
  const open = new Set(orders.map((o) => o.id));
  orders.forEach((o) => upsertOrder(o.id, "Updated", o));
  markClosedOnPlatform({ except: open });
}
JSON200 OK (shortened)
[
  {
    "schemaVersion": "2.0",
    "id": "3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40",
    "platform": "Mock",
    "platformOrderId": "mock-261015123000-1",
    "displayCode": "TEST-1",
    "state": "Received",
    "channel": "Delivery",
    "…": "see the full examples below"
  }
]

Canonical order

Every platform's order is normalized into one model. Your POS parses one format and never branches on the platform.

JSON conventions

  • camelCase property names; enums are strings ("Delivery", never 1).
  • Null values are omitted. A missing property means "unknown / not provided". Give every optional field a default.
  • Money is an integer in minor units (cents): {"amountMinor": 890, "currency": "EUR"} is €8.90. Amounts in amounts are plain integers in amounts.currency.
  • Timestamps are ISO 8601 with an offset, usually UTC: "2026-10-15T12:30:00+00:00".
  • Ignore unknown properties and accept unknown enum values. New, optional fields are added without notice (see versioning).
  • Prices are gross (VAT included) as the customer paid them, before discounts.

Order fields

FieldTypeDescription
schemaVersionstringOrder schema version, currently "2.0".
idkeyuuidRapidHub order id. Stable for the life of the order. Use it in POST /pos/orders/{id}/state.
platformenumPlatformKey: Wolt, UberEats, DeliveryHero, JustEatTakeaway, Glovo, Mock …
platformOrderIdstringThe platform's internal order id. Unique per platform.
displayCodestring?Short code the customer and courier see (e.g. "8LAB2Z"). Print it large on the ticket.
environmentenumStaging or Production (of the connection).
tenantRefstringTenant reference.
storeobjectplatformStoreId (store id on the platform) and restaurantId (uuid of your restaurant).
channelenumDelivery, Pickup or DineIn.
fulfillmentobjectcourierType (Platform · Self · Marketplace), promisedFor (when the customer expects the order, or the pickup time; for pre-orders the scheduled time), preparationDeadline, deliveryAddress.
fulfillment.deliveryAddressobject?street, houseNumber, zip, city, lat, lng, notes. All optional. Present for delivery orders, most useful with courierType: "Self".
stateenumCanonical state, see the state machine.
platformStatestring?Raw platform status (audit only, do not branch on it).
timestampsobjectplacedAt (customer placed it), receivedByHubAt, acceptedAt (set on the first transition to Accepted).
customerobject?name, phone (often a platform proxy number), note (order note for the kitchen / courier).
itemsarrayOrder lines, see items & modifiers.
amountsobjectTotals in minor units, see amounts.
paymentobjectstatus, method, cashToCollect.
discounts v2array?Discount breakdown; the sum equals amounts.discounts. Missing = the platform gives no breakdown.
fees v2array?Fee breakdown (delivery, service, small order …).
timing v2enumInstant or Scheduled (pre-order; prepare for fulfillment.promisedFor).
acceptance v2enumByVenue (you must accept) or AutoByPlatform (the platform accepts; show no accept button).
acceptDeadlinedatetime?Accept before this time or the platform cancels the order. Missing = no known deadline.
rejectReasonstring?Reason text of a rejection or cancellation.
platformExtras v2object?String-to-string map of display-only details: courierName, cashGiven (cash the customer hands over, minor units), venueName, deliveryStatus, noContactDelivery, companyName, companyTaxId, loyaltyCard, prepTimeSeconds, mock … Show them; never branch business logic on them (exception: mock, see sandbox).
signatureVerifiedbooleanThe platform webhook signature was verified.
notificationOnlybooleanInternal; always false for orders delivered to the POS.
rawPayloadstring?The platform's original order JSON, for support and debugging. Do not parse it: its format belongs to the platform and changes without notice. It can be large and contains personal data; do not log it.

Items & modifiers

FieldTypeDescription
items[].lineIdstringLine id, unique within the order. Referenced by item discounts.
items[].refobjectProduct reference for mapping to your POS: sku (your SKU from the menu push), plu, barcode, platformItemId. Map by sku; fall back to the name and flag the line if the SKU is unknown.
items[].namestringName as the customer saw it.
items[].quantityintQuantity.
items[].unitPriceMoneyPrice of one unit without modifiers, before discounts.
items[].modifiersarraySelected options. Always present (may be empty).
items[].notestring?Line note ("well done").
items[].taxRatePercent v2number?VAT rate the platform reported. Missing = use your own rate for the SKU.
items[].deposit v2object?Deposit (e.g. German Pfand) per unit: unitAmount (Money) and taxRatePercent. Not included in unitPrice; print it as its own receipt line.
items[].discount v2Money?Total discount on this line (all units, modifiers included).
items[].lineTotal v2Money?The platform's line total: (unitPrice + Σ modifier price × qty) × quantity, before discounts, without deposits. Use it to check your own calculation.
modifiers[].namestringOption name. Removals ("no onions") arrive as normal modifiers priced 0, typically named Ohne … / No ….
modifiers[].quantityintQuantity of this option per unit of the item.
modifiers[].priceMoneyPrice of one unit of the option.
modifiers[].groupNamestring?Modifier group, or the combo step ("Drink").
modifiers[].sku v2string?The option's SKU from your menu push. For combo steps this is the product SKU.
modifiers[].taxRatePercent v2number?VAT rate of the option, if reported.
modifiers[].deposit v2object?Deposit of the option (e.g. the drink inside a combo), per unit.

Amounts & payment

FieldTypeDescription
amounts.currencystringISO 4217 code for every amount of the order.
amounts.itemsSubtotalint64Sum of the lines (with modifiers, before discounts, without deposits).
amounts.deliveryFeeint64Delivery fee charged to the customer (0 for pickup).
amounts.discountsint64Total of all discounts, as a positive number.
amounts.tipint64Tip.
amounts.taxint64VAT included in the order, as reported by the platform. 0 can mean "not reported" (e.g. Wolt); compute VAT from the line rates.
amounts.totalint64The order total as reported by the platform.
payment.statusenumPaidOnline, CashOnDelivery, CardOnDelivery or Unknown. Informational: fiscal handling happens in your POS.
payment.methodstring?Platform payment method name (free text).
payment.cashToCollect v2int64?Cash your own courier or counter must collect (self-delivery / pickup with cash). With a platform courier the courier collects cash, so the order counts as PaidOnline for you.
ⓘ What amounts.total contains

total is the platform's own figure, and platforms define it differently. On the Mock platform it is itemsSubtotal + deposits + Σ fees − Σ discounts + tip. Wolt reports the basket total after discounts, without platform fees and tip. Build receipts from items, discounts, fees and tip, and use total as a cross-check and as the amount to collect.

Discounts & fees

FieldTypeDescription
discounts[].scopeenumItem (one line, see lineId), Basket or Delivery.
discounts[].amountMoneyPositive amount subtracted from the total.
discounts[].fundedByenumWho pays for it: Venue, Platform, Shared or Unknown. Only the venue-funded part reduces your revenue.
discounts[].lineIdstring?For scope: Item: the discounted line.
discounts[].namestring?Campaign / voucher name.
fees[].typestringdelivery, service, small_order, priority, delivery_discount (Wolt) or other. Treat unknown values as other.
fees[].amountMoneyFee amount.
fees[].venueRevenueboolean?Whether the fee is your revenue (e.g. your own delivery fee). Missing = unknown; platform fees usually are not.
fees[].taxRatePercentnumber?VAT rate of the fee, if reported.
fees[].platformTypestring?Raw platform fee type (audit).

Enumerations

All enums are serialized as strings. New values can be added, so handle unknown values gracefully.

EnumValues
stateReceived · Accepted · Preparing · Ready · Dispatched · Delivered · Rejected · Canceled
platformWolt · UberEats · DeliveryHero (foodora, Glovo on the DH API …) · JustEatTakeaway (Just Eat, Lieferando, Takeaway.com …) · Glovo (legacy Glovo API) · Deliveroo (reserved) · Mock (sandbox)
environmentStaging · Production
channelDelivery · Pickup · DineIn
fulfillment.courierTypePlatform (platform courier) · Self (your own courier delivers) · Marketplace (the venue hands over / delivers, e.g. pickup orders)
payment.statusUnknown · PaidOnline · CashOnDelivery · CardOnDelivery
timingInstant · Scheduled
acceptanceByVenue · AutoByPlatform
discounts[].scopeBasket · Item · Delivery
discounts[].fundedByUnknown · Venue · Platform · Shared
rejectCode (request)Other · ItemsUnavailable · TooBusy · Closing · Closed
eventKindReceived · Updated · Canceled

Example orders

Real orders produced by the Mock platform from the sample menu. Amounts reconcile exactly. Download them for your tests: delivery-platform.json, self-cash.json, pickup-discount.json, combo-deposit.json, scheduled-auto.json.

{
  "schemaVersion": "2.0",
  "id": "3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40",
  "platform": "Mock",
  "platformOrderId": "mock-261015123000-1",
  "displayCode": "TEST-1",
  "environment": "Staging",
  "tenantRef": "acme-pos:tenant:1042",
  "store": {
    "platformStoreId": "mock-3f9a1c2e",
    "restaurantId": "b5e1f0d2-8c3a-4e7b-9f1d-6a2c4e8b0d13"
  },
  "channel": "Delivery",
  "fulfillment": {
    "courierType": "Platform",
    "promisedFor": "2026-10-15T13:00:00+00:00",
    "deliveryAddress": {
      "street": "Teststraße",
      "houseNumber": "1",
      "zip": "10115",
      "city": "Berlin",
      "lat": 52.5321,
      "lng": 13.3849,
      "notes": "2nd floor, ring \"Sandbox\""
    }
  },
  "state": "Received",
  "platformState": "mock:created",
  "timestamps": {
    "placedAt": "2026-10-15T12:30:00+00:00",
    "receivedByHubAt": "2026-10-15T12:30:00+00:00"
  },
  "customer": {
    "name": "Test Customer",
    "phone": "+49 30 1234567"
  },
  "items": [
    {
      "lineId": "1",
      "ref": {
        "sku": "BURGER",
        "plu": "BURGER",
        "platformItemId": "mock-item-BURGER"
      },
      "name": "Cheeseburger",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 890,
        "currency": "EUR"
      },
      "modifiers": [
        {
          "name": "Extra cheese",
          "quantity": 1,
          "price": {
            "amountMinor": 100,
            "currency": "EUR"
          },
          "groupName": "Extras",
          "sku": "X-CHEESE",
          "taxRatePercent": 7
        },
        {
          "name": "Ohne Onions",
          "quantity": 1,
          "price": {
            "amountMinor": 0,
            "currency": "EUR"
          },
          "groupName": "Extras",
          "sku": "NO-X-ONION",
          "taxRatePercent": 7
        }
      ],
      "taxRatePercent": 7,
      "lineTotal": {
        "amountMinor": 990,
        "currency": "EUR"
      }
    },
    {
      "lineId": "2",
      "ref": {
        "sku": "FRIES",
        "plu": "FRIES",
        "platformItemId": "mock-item-FRIES"
      },
      "name": "Fries",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 350,
        "currency": "EUR"
      },
      "modifiers": [],
      "taxRatePercent": 7,
      "lineTotal": {
        "amountMinor": 350,
        "currency": "EUR"
      }
    }
  ],
  "amounts": {
    "currency": "EUR",
    "itemsSubtotal": 1340,
    "deliveryFee": 299,
    "discounts": 0,
    "tip": 200,
    "tax": 88,
    "total": 1839
  },
  "payment": {
    "status": "PaidOnline",
    "method": "mock_online"
  },
  "discounts": [],
  "fees": [
    {
      "type": "delivery",
      "amount": {
        "amountMinor": 299,
        "currency": "EUR"
      },
      "venueRevenue": false,
      "taxRatePercent": 19,
      "platformType": "mock_delivery_fee"
    }
  ],
  "timing": "Instant",
  "acceptance": "ByVenue",
  "platformExtras": {
    "mock": "1",
    "venueName": "RapidHub Sandbox",
    "courierName": "Test Courier"
  },
  "signatureVerified": true,
  "notificationOnly": false,
  "acceptDeadline": "2026-10-15T12:33:00+00:00"
}
{
  "schemaVersion": "2.0",
  "id": "8a41f6c3-2d9e-4b75-a0c8-1e7f3b9d5c62",
  "platform": "Mock",
  "platformOrderId": "mock-261015123000-2",
  "displayCode": "TEST-2",
  "environment": "Staging",
  "tenantRef": "acme-pos:tenant:1042",
  "store": {
    "platformStoreId": "mock-3f9a1c2e",
    "restaurantId": "b5e1f0d2-8c3a-4e7b-9f1d-6a2c4e8b0d13"
  },
  "channel": "Delivery",
  "fulfillment": {
    "courierType": "Self",
    "promisedFor": "2026-10-15T13:00:00+00:00",
    "deliveryAddress": {
      "street": "Teststraße",
      "houseNumber": "1",
      "zip": "10115",
      "city": "Berlin",
      "lat": 52.5321,
      "lng": 13.3849,
      "notes": "2nd floor, ring \"Sandbox\""
    }
  },
  "state": "Received",
  "platformState": "mock:created",
  "timestamps": {
    "placedAt": "2026-10-15T12:30:00+00:00",
    "receivedByHubAt": "2026-10-15T12:30:00+00:00"
  },
  "customer": {
    "name": "Test Customer",
    "phone": "+49 30 1234567",
    "note": "Please ring twice. (test note)"
  },
  "items": [
    {
      "lineId": "1",
      "ref": {
        "sku": "BURGER",
        "plu": "BURGER",
        "platformItemId": "mock-item-BURGER"
      },
      "name": "Cheeseburger",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 890,
        "currency": "EUR"
      },
      "modifiers": [],
      "note": "Extra crispy please (test item note)",
      "taxRatePercent": 7,
      "lineTotal": {
        "amountMinor": 890,
        "currency": "EUR"
      }
    },
    {
      "lineId": "2",
      "ref": {
        "sku": "COLA",
        "plu": "COLA",
        "platformItemId": "mock-item-COLA"
      },
      "name": "Cola 0.33 l",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 250,
        "currency": "EUR"
      },
      "modifiers": [],
      "taxRatePercent": 19,
      "deposit": {
        "unitAmount": {
          "amountMinor": 25,
          "currency": "EUR"
        },
        "taxRatePercent": 19
      },
      "lineTotal": {
        "amountMinor": 250,
        "currency": "EUR"
      }
    }
  ],
  "amounts": {
    "currency": "EUR",
    "itemsSubtotal": 1140,
    "deliveryFee": 299,
    "discounts": 0,
    "tip": 0,
    "tax": 102,
    "total": 1464
  },
  "payment": {
    "status": "CashOnDelivery",
    "method": "cash",
    "cashToCollect": 1464
  },
  "discounts": [],
  "fees": [
    {
      "type": "delivery",
      "amount": {
        "amountMinor": 299,
        "currency": "EUR"
      },
      "venueRevenue": true,
      "taxRatePercent": 19,
      "platformType": "mock_delivery_fee"
    }
  ],
  "timing": "Instant",
  "acceptance": "ByVenue",
  "platformExtras": {
    "mock": "1",
    "venueName": "RapidHub Sandbox",
    "cashGiven": "2000"
  },
  "signatureVerified": true,
  "notificationOnly": false,
  "acceptDeadline": "2026-10-15T12:33:00+00:00"
}
{
  "schemaVersion": "2.0",
  "id": "e2b7c9a1-6f3d-4c08-b5e4-9d1a7c3f8e25",
  "platform": "Mock",
  "platformOrderId": "mock-261015123000-3",
  "displayCode": "TEST-3",
  "environment": "Staging",
  "tenantRef": "acme-pos:tenant:1042",
  "store": {
    "platformStoreId": "mock-3f9a1c2e",
    "restaurantId": "b5e1f0d2-8c3a-4e7b-9f1d-6a2c4e8b0d13"
  },
  "channel": "Pickup",
  "fulfillment": {
    "courierType": "Marketplace",
    "promisedFor": "2026-10-15T12:50:00+00:00"
  },
  "state": "Received",
  "platformState": "mock:created",
  "timestamps": {
    "placedAt": "2026-10-15T12:30:00+00:00",
    "receivedByHubAt": "2026-10-15T12:30:00+00:00"
  },
  "customer": {
    "name": "Test Customer",
    "phone": "+49 30 1234567"
  },
  "items": [
    {
      "lineId": "1",
      "ref": {
        "sku": "BURGER",
        "plu": "BURGER",
        "platformItemId": "mock-item-BURGER"
      },
      "name": "Cheeseburger",
      "quantity": 2,
      "unitPrice": {
        "amountMinor": 890,
        "currency": "EUR"
      },
      "modifiers": [],
      "taxRatePercent": 7,
      "discount": {
        "amountMinor": 178,
        "currency": "EUR"
      },
      "lineTotal": {
        "amountMinor": 1780,
        "currency": "EUR"
      }
    },
    {
      "lineId": "2",
      "ref": {
        "sku": "FRIES",
        "plu": "FRIES",
        "platformItemId": "mock-item-FRIES"
      },
      "name": "Fries",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 350,
        "currency": "EUR"
      },
      "modifiers": [],
      "taxRatePercent": 7,
      "lineTotal": {
        "amountMinor": 350,
        "currency": "EUR"
      }
    }
  ],
  "amounts": {
    "currency": "EUR",
    "itemsSubtotal": 2130,
    "deliveryFee": 0,
    "discounts": 378,
    "tip": 0,
    "tax": 115,
    "total": 1752
  },
  "payment": {
    "status": "PaidOnline",
    "method": "mock_online"
  },
  "discounts": [
    {
      "scope": "Item",
      "amount": {
        "amountMinor": 178,
        "currency": "EUR"
      },
      "fundedBy": "Venue",
      "lineId": "1",
      "name": "10% item discount (test)"
    },
    {
      "scope": "Basket",
      "amount": {
        "amountMinor": 200,
        "currency": "EUR"
      },
      "fundedBy": "Shared",
      "name": "Test voucher"
    }
  ],
  "fees": [],
  "timing": "Instant",
  "acceptance": "ByVenue",
  "platformExtras": {
    "mock": "1",
    "venueName": "RapidHub Sandbox"
  },
  "signatureVerified": true,
  "notificationOnly": false,
  "acceptDeadline": "2026-10-15T12:33:00+00:00"
}
{
  "schemaVersion": "2.0",
  "id": "5f8e1d3b-9c2a-47e6-8b0f-3d6a2e9c1b74",
  "platform": "Mock",
  "platformOrderId": "mock-261015123000-4",
  "displayCode": "TEST-4",
  "environment": "Staging",
  "tenantRef": "acme-pos:tenant:1042",
  "store": {
    "platformStoreId": "mock-3f9a1c2e",
    "restaurantId": "b5e1f0d2-8c3a-4e7b-9f1d-6a2c4e8b0d13"
  },
  "channel": "Delivery",
  "fulfillment": {
    "courierType": "Platform",
    "promisedFor": "2026-10-15T13:00:00+00:00",
    "deliveryAddress": {
      "street": "Teststraße",
      "houseNumber": "1",
      "zip": "10115",
      "city": "Berlin",
      "lat": 52.5321,
      "lng": 13.3849,
      "notes": "2nd floor, ring \"Sandbox\""
    }
  },
  "state": "Received",
  "platformState": "mock:created",
  "timestamps": {
    "placedAt": "2026-10-15T12:30:00+00:00",
    "receivedByHubAt": "2026-10-15T12:30:00+00:00"
  },
  "customer": {
    "name": "Test Customer",
    "phone": "+49 30 1234567"
  },
  "items": [
    {
      "lineId": "1",
      "ref": {
        "sku": "COLA",
        "plu": "COLA",
        "platformItemId": "mock-item-COLA"
      },
      "name": "Cola 0.33 l",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 250,
        "currency": "EUR"
      },
      "modifiers": [],
      "taxRatePercent": 19,
      "deposit": {
        "unitAmount": {
          "amountMinor": 25,
          "currency": "EUR"
        },
        "taxRatePercent": 19
      },
      "lineTotal": {
        "amountMinor": 250,
        "currency": "EUR"
      }
    },
    {
      "lineId": "2",
      "ref": {
        "sku": "MENU1",
        "plu": "MENU1",
        "platformItemId": "mock-item-MENU1"
      },
      "name": "Burger Menu",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 1190,
        "currency": "EUR"
      },
      "modifiers": [
        {
          "name": "Cheeseburger",
          "quantity": 1,
          "price": {
            "amountMinor": 0,
            "currency": "EUR"
          },
          "groupName": "Main",
          "sku": "BURGER",
          "taxRatePercent": 7
        },
        {
          "name": "Fries",
          "quantity": 1,
          "price": {
            "amountMinor": 0,
            "currency": "EUR"
          },
          "groupName": "Side",
          "sku": "FRIES",
          "taxRatePercent": 7
        },
        {
          "name": "Cola 0.33 l",
          "quantity": 1,
          "price": {
            "amountMinor": 0,
            "currency": "EUR"
          },
          "groupName": "Drink",
          "sku": "COLA",
          "taxRatePercent": 19,
          "deposit": {
            "unitAmount": {
              "amountMinor": 25,
              "currency": "EUR"
            },
            "taxRatePercent": 19
          }
        }
      ],
      "taxRatePercent": 7,
      "lineTotal": {
        "amountMinor": 1190,
        "currency": "EUR"
      }
    }
  ],
  "amounts": {
    "currency": "EUR",
    "itemsSubtotal": 1440,
    "deliveryFee": 299,
    "discounts": 299,
    "tip": 0,
    "tax": 126,
    "total": 1490
  },
  "payment": {
    "status": "PaidOnline",
    "method": "mock_online"
  },
  "discounts": [
    {
      "scope": "Delivery",
      "amount": {
        "amountMinor": 299,
        "currency": "EUR"
      },
      "fundedBy": "Platform",
      "name": "Free delivery (test)"
    }
  ],
  "fees": [
    {
      "type": "delivery",
      "amount": {
        "amountMinor": 299,
        "currency": "EUR"
      },
      "venueRevenue": false,
      "taxRatePercent": 19,
      "platformType": "mock_delivery_fee"
    }
  ],
  "timing": "Instant",
  "acceptance": "ByVenue",
  "platformExtras": {
    "mock": "1",
    "venueName": "RapidHub Sandbox",
    "courierName": "Test Courier"
  },
  "signatureVerified": true,
  "notificationOnly": false,
  "acceptDeadline": "2026-10-15T12:33:00+00:00"
}
{
  "schemaVersion": "2.0",
  "id": "0b6c4e2a-7d1f-4a93-9e5b-8c2f1a6d3e07",
  "platform": "Mock",
  "platformOrderId": "mock-261015123000-5",
  "displayCode": "TEST-5",
  "environment": "Staging",
  "tenantRef": "acme-pos:tenant:1042",
  "store": {
    "platformStoreId": "mock-3f9a1c2e",
    "restaurantId": "b5e1f0d2-8c3a-4e7b-9f1d-6a2c4e8b0d13"
  },
  "channel": "Pickup",
  "fulfillment": {
    "courierType": "Marketplace",
    "promisedFor": "2026-10-15T15:30:00+00:00"
  },
  "state": "Accepted",
  "platformState": "mock:auto_accepted",
  "timestamps": {
    "placedAt": "2026-10-15T12:30:00+00:00",
    "receivedByHubAt": "2026-10-15T12:30:00+00:00",
    "acceptedAt": "2026-10-15T12:30:00+00:00"
  },
  "customer": {
    "name": "Test Customer",
    "phone": "+49 30 1234567"
  },
  "items": [
    {
      "lineId": "1",
      "ref": {
        "sku": "FRIES",
        "plu": "FRIES",
        "platformItemId": "mock-item-FRIES"
      },
      "name": "Fries",
      "quantity": 1,
      "unitPrice": {
        "amountMinor": 350,
        "currency": "EUR"
      },
      "modifiers": [],
      "taxRatePercent": 7,
      "lineTotal": {
        "amountMinor": 350,
        "currency": "EUR"
      }
    }
  ],
  "amounts": {
    "currency": "EUR",
    "itemsSubtotal": 350,
    "deliveryFee": 0,
    "discounts": 0,
    "tip": 0,
    "tax": 23,
    "total": 350
  },
  "payment": {
    "status": "PaidOnline",
    "method": "mock_online"
  },
  "discounts": [],
  "fees": [],
  "timing": "Scheduled",
  "acceptance": "AutoByPlatform",
  "platformExtras": {
    "mock": "1",
    "venueName": "RapidHub Sandbox"
  },
  "signatureVerified": true,
  "notificationOnly": false
}
ExampleWhat to look at
Delivery · platform courierModifiers with a paid extra and a removal (Ohne Onions, price 0); delivery fee in fees with venueRevenue: false; tip; acceptDeadline 3 minutes after placement.
Own courier · cashcourierType: "Self", payment.status: "CashOnDelivery", cashToCollect = total; platformExtras.cashGiven for change; deposit on the cola; item note and customer note.
Pickup · discountschannel: "Pickup", no fees; quantity 2; an Item discount (venue-funded, lineId: "1", also in items[0].discount) and a Basket voucher (shared).
Combo · deposit · free deliveryA combo is one line whose modifiers are the steps (groupName = step, sku = product SKU, price = surcharge); the drink step carries its own deposit; free delivery as a Delivery discount funded by the platform.
Pre-order · auto-acceptedtiming: "Scheduled" with promisedFor 3 hours ahead; acceptance: "AutoByPlatform", arrives as Accepted with acceptedAt and no acceptDeadline.

The Mock platform's amounts satisfy:

TextReconciliation (Mock)
itemsSubtotal = Σ items[].lineTotal                       (unit + modifiers) × quantity, before discounts
discounts     = Σ discounts[].amount
total         = itemsSubtotal + deposits + Σ fees[] − discounts + tip
deposits      = Σ (items[].deposit + Σ modifiers[].deposit × modifier qty) × item quantity
tax           = VAT included in the lines and deposits (each at its own rate)

State machine

All platforms share one lifecycle. RapidHub translates each transition into the platform's own call (accept, ready, picked up …) and rejects transitions the machine does not allow.

skip steps: Accepted → Ready · Dispatched · Delivered … Received Accepted Preparing Ready Dispatcheddelivery only Delivered Rejected Canceledfrom any open state reject (from Received only) Delivered · Rejected · Canceled are terminal
Solid: the usual path. Dashed: allowed shortcuts (a customer can pay and leave before you press "Ready").
FromAllowed targets
ReceivedAccepted · Rejected · Canceled
AcceptedPreparing · Ready · Dispatched · Delivered · Canceled
PreparingReady · Dispatched · Delivered · Canceled
ReadyDispatched · Delivered · Canceled
DispatchedDelivered · Canceled
Delivered · Rejected · Cancelednone (terminal)
  • Dispatched is for delivery orders. For Pickup / DineIn go from Ready straight to Delivered (handed over).
  • Platform couriers: the platform reports pickup and delivery itself; you receive them as Updated events. Sending Dispatched / Delivered for a platform-courier order is accepted and recorded in RapidHub, but not forwarded.
  • Preparing is optional and not forwarded to most platforms.

Change an order's state

POST/pos/orders/{orderId}/stateX-Restaurant-Key

RapidHub checks the transition, calls the platform, and only then stores the new state and sends an Updated event. If the platform call fails, nothing changes.

Body fieldTypeDescription
staterequiredstringTarget state (case-insensitive): Accepted, Rejected, Preparing, Ready, Dispatched, Delivered, Canceled.
prepTimeMinutesoptionalintWith Accepted: minutes until the food is ready (0–240). Omit to keep the platform's estimate. Needs capabilities.supportsPrepTime.
rejectCodeoptionalstringWith Rejected: Other, ItemsUnavailable, TooBusy, Closing, Closed. Needs capabilities.supportsRejectCodes.
reasonoptionalstringWith Rejected / Canceled: free text. Wolt shows it to the customer.
curl -X POST https://hub.rapidpaygroup.com/pos/orders/3c9d2b7e-41a8-4f0b-8e6d-5a1c9f2e7b40/state \
  -H "X-Restaurant-Key: $RAPIDHUB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"state":"Accepted","prepTimeMinutes":20}'
using System.Net.Http.Json;

var res = await http.PostAsJsonAsync($"/pos/orders/{orderId}/state",
    new { state = "Accepted", prepTimeMinutes = 20 });

switch ((int)res.StatusCode)
{
    case 200:                       // {"status":"ok","state":"Accepted"}
        break;
    case 409:                       // invalid_transition | state_mismatch
        await CatchUpAsync();       // re-read the order, then decide
        break;
    case 502:                       // platform call failed, nothing changed: retry with backoff
        ScheduleRetry(orderId, "Accepted");
        break;
    default:
        res.EnsureSuccessStatusCode();
        break;
}
@Serializable
data class StateRequest(
    val state: String,
    val prepTimeMinutes: Int? = null,
    val rejectCode: String? = null,
    val reason: String? = null,
)

val res = http.post("pos/orders/$orderId/state") {
    contentType(ContentType.Application.Json)
    setBody(StateRequest(state = "Accepted", prepTimeMinutes = 20))
}
when (res.status.value) {
    200 -> Unit
    409 -> catchUp()                      // invalid_transition / state_mismatch
    502 -> scheduleRetry(orderId)         // platform unreachable, nothing changed
    else -> error("set state failed: ${res.status} ${res.bodyAsText()}")
}
const res = await fetch(`${BASE}/pos/orders/${orderId}/state`, {
  method: "POST",
  headers: { "X-Restaurant-Key": key, "Content-Type": "application/json" },
  body: JSON.stringify({ state: "Accepted", prepTimeMinutes: 20 }),
});
if (res.status === 409) {
  const { error, detail, upstream } = await res.json(); // invalid_transition | state_mismatch
  await catchUp();
} else if (res.status === 502) {
  scheduleRetry(orderId);                               // nothing changed on the platform
} else if (!res.ok) {
  throw new Error(`state change failed: ${res.status}`);
}
JSONReject
{
  "state": "Rejected",
  "rejectCode": "ItemsUnavailable",
  "reason": "Sorry, the cheeseburger is sold out."
}
JSON200 OK
{
  "status": "ok",
  "state": "Accepted"
}

Accepting

  • Watch acceptDeadline. Show a countdown. If you do not accept in time the platform cancels the order and you receive a Canceled event. Typical windows: Wolt 3 minutes (pre-orders 15 minutes), Uber Eats about 11 minutes after RapidHub received the order, Delivery Hero as set by the platform per order.
  • acceptance: "AutoByPlatform": the platform accepted the order already (it usually arrives as Accepted). Show no accept / reject buttons; just prepare it.
  • Auto-accept in RapidHub. A connection can be set in the dashboard to accept orders immediately, or shortly before the deadline as a safety net. You then receive an Updated event with state: "Accepted".
  • Pre-orders (timing: "Scheduled"): accept them like any order; prepare for fulfillment.promisedFor. On Wolt, accepting a pre-order confirms it.
  • Preparation time: send prepTimeMinutes with Accepted when capabilities.supportsPrepTime is true. Platforms may clamp it (Wolt: at most 25 minutes later than its own estimate).

Rejecting & cancelling

  • Rejected is only allowed from Received. After accepting, use Canceled.
  • Several platforms have no API for a venue to cancel an accepted order (Wolt, Delivery Hero). The cancel is then applied in RapidHub only; the restaurant must also cancel through the platform's merchant tools or support.
rejectCodeWoltDelivery HeroUber Eats
ItemsUnavailableITEMS_UNAVAILABLEITEM_UNAVAILABLECodes not supported: always STORE_UNAVAILABLE
TooBusyGENERICTOO_BUSY
Closing / ClosedVENUE_CLOSING_SOONCLOSED
Other / noneGENERICTOO_BUSY
Test ordersn/aalways TEST_ORDER

Wolt requires a reason text and shows it to the customer. Without reason, RapidHub sends a polite default text in German.

State errors

HTTPBodyMeaning and what to do
400{"error":"invalid_state","state":"Done"}Unknown state. Also invalid_reject_code, invalid_prep_time (outside 0–240).
401emptyBad restaurant key.
403{"error":"not_your_order"}The order belongs to another restaurant.
404emptyUnknown order id. Use id, not platformOrderId.
409{"error":"invalid_transition","detail":"Received→Ready"}The state machine does not allow it, including sending the state the order already has. If the order is already in the target state, treat it as success.
409{"error":"state_mismatch","upstream":"CANCELLED"}The platform has the order in another state (raw platform status in upstream). If the platform already closed it, RapidHub updates the order and sends you an event. Re-read the order.
502{"error":"failed","detail":"…"}The platform call failed (network, platform error). Local state unchanged; retry with backoff, then tell the staff to use the platform tablet.

Connections & capabilities

GET/pos/catalog/connectionsX-Restaurant-Key

Lists the tenant's platform connections. Build your "delivery platforms" settings screen from it, and decide which actions to offer from capabilities, not from the platform name.

JSON200 OK
[
  {
    "id": "7d0c2a4e-5b1f-4c8e-9a3d-2f6b8e1c4a70",
    "platform": "Mock",
    "platformStoreId": "mock-3f9a1c2e",
    "environment": "Staging",
    "brand": null,
    "isActive": true,
    "capabilities": {
      "supportsPrepTime": true,
      "supportsRejectCodes": true,
      "supportsStoreStatus": true,
      "supportsItemAvailability": true,
      "menuPush": "Full"
    }
  },
  {
    "id": "1e5a9c3f-7b2d-4e8a-b6c0-4f9d2a7e1c38",
    "platform": "Wolt",
    "platformStoreId": "65f1c0a2b3d4e5f6a7b8c9d0",
    "environment": "Production",
    "brand": null,
    "isActive": true,
    "capabilities": {
      "supportsPrepTime": true,
      "supportsRejectCodes": true,
      "supportsStoreStatus": true,
      "supportsItemAvailability": true,
      "menuPush": "Full"
    }
  },
  {
    "id": "9b3e7d1a-4c6f-4a2b-8d5e-0f1a3c5e7b92",
    "platform": "UberEats",
    "platformStoreId": "0f6a3c1e-2b4d-4e8f-9a7c-5d3b1e9f7a24",
    "environment": "Production",
    "brand": null,
    "isActive": true,
    "capabilities": {
      "supportsPrepTime": false,
      "supportsRejectCodes": false,
      "supportsStoreStatus": false,
      "supportsItemAvailability": false,
      "menuPush": "None"
    }
  }
]
CapabilityMeaning
supportsPrepTimeprepTimeMinutes on accept is forwarded.
supportsRejectCodesrejectCode is mapped to a platform code.
supportsStoreStatusOpen/pause and status reading work.
supportsItemAvailabilityItem updates (sold out / price) work.
menuPushFull = full menu push + item updates; UpdatesOnly = the menu is managed on the platform, only item updates by SKU; None = no menu actions.
PlatformPrep timeReject codesStore statusItem updatesMenu push
WoltyesyesyesyesFull
Delivery HeroyesyesyesyesFull
MockyesyesyesyesFull
Uber EatsnonononoNone
Just Eat Takeaway · Glovo (legacy)nonononoNone

The table shows what the API reports today; always read capabilities at runtime.

POST/pos/catalog/pushX-Restaurant-Key

Send the menu once; RapidHub converts it for every target platform (prices, VAT, deposits, allergens, food info, combos) and returns one job per connection.

  • connectionIds omitted or empty = all active connections of the tenant. Pass explicit ids for a single restaurant.
  • A push replaces the menu on the platform.
  • Prices are gross, in minor units. currency defaults to EUR.
  • Partial failure is normal: one platform can accept while another rejects. Check every job.
  • The last pushed menu per connection is visible in the dashboard (Restaurants → Menu), with errors and warnings.
curl -X POST https://hub.rapidpaygroup.com/pos/catalog/push \
  -H "X-Restaurant-Key: $RAPIDHUB_KEY" \
  -H "Content-Type: application/json" \
  -d @menu.json
var menu = new
{
    connectionIds = new[] { connectionId },
    currency = "EUR",
    products = new[]
    {
        new { sku = "BURGER", name = "Cheeseburger", priceMinor = 890L, taxRatePercent = 7m,
              modifierGroupIds = new[] { "G-EXTRAS" } },
        // …
    },
    categories = new[] { new { id = "cat-burgers", name = "Burgers", itemSkus = new[] { "BURGER" } } },
    modifierGroups = new[]
    {
        new { id = "G-EXTRAS", name = "Extras", minSelect = 0, maxSelect = 3,
              options = new[] { new { sku = "X-CHEESE", name = "Extra cheese", priceMinor = 100L } } },
    },
};
var res = await http.PostAsJsonAsync("/pos/catalog/push", menu);
var result = await res.Content.ReadFromJsonAsync<JsonElement>();
foreach (var job in result.GetProperty("jobs").EnumerateArray())
    Console.WriteLine($"{job.GetProperty("platform")}: {job.GetProperty("status")} {job.GetProperty("lastError")}");
@Serializable data class MenuItem(val sku: String, val name: String, val priceMinor: Long,
    val taxRatePercent: Double? = null, val available: Boolean = true,
    val modifierGroupIds: List<String>? = null, val depositMinor: Long? = null)
@Serializable data class Category(val id: String, val name: String, val itemSkus: List<String>)
@Serializable data class PushRequest(val connectionIds: List<String>?, val currency: String,
    val products: List<MenuItem>, val categories: List<Category>? = null)

val res = http.post("pos/catalog/push") {
    contentType(ContentType.Application.Json)
    setBody(PushRequest(listOf(connectionId), "EUR",
        products = listOf(MenuItem("BURGER", "Cheeseburger", 890, taxRatePercent = 7.0)),
        categories = listOf(Category("cat-burgers", "Burgers", listOf("BURGER")))))
}
val jobs = json.parseToJsonElement(res.bodyAsText()).jsonObject["jobs"]!!.jsonArray
const res = await fetch(`${BASE}/pos/catalog/push`, {
  method: "POST",
  headers: { "X-Restaurant-Key": key, "Content-Type": "application/json" },
  body: JSON.stringify(menu), // see the sample menu below
});
const { jobs } = await res.json();
for (const job of jobs) console.log(job.platform, job.status, job.lastError ?? "");
JSONmenu.json — the sample menu used throughout these docs
{
  "connectionIds": [
    "7d0c2a4e-5b1f-4c8e-9a3d-2f6b8e1c4a70"
  ],
  "currency": "EUR",
  "products": [
    {
      "sku": "BURGER",
      "name": "Cheeseburger",
      "description": "Beef patty, cheddar, pickles, house sauce",
      "priceMinor": 890,
      "taxRatePercent": 7,
      "imageUrl": "https://example.com/img/cheeseburger.jpg",
      "modifierGroupIds": [
        "G-EXTRAS"
      ],
      "allergens": [
        "Gluten",
        "Milk",
        "Mustard"
      ]
    },
    {
      "sku": "FRIES",
      "name": "Fries",
      "priceMinor": 350,
      "taxRatePercent": 7,
      "foodInfo": {
        "vegetarian": true
      }
    },
    {
      "sku": "COLA",
      "name": "Cola 0.33 l",
      "priceMinor": 250,
      "taxRatePercent": 19,
      "depositMinor": 25,
      "depositTaxRatePercent": 19,
      "foodInfo": {
        "additives": [
          "Mit Farbstoff(en)",
          "Koffeinhaltig"
        ],
        "caffeineMgPer100ml": 10,
        "netAmount": 330,
        "netUnit": "ml",
        "prepackaged": true
      }
    },
    {
      "sku": "MENU1",
      "name": "Burger Menu",
      "description": "Cheeseburger, fries and a drink",
      "priceMinor": 1190,
      "taxRatePercent": 7,
      "modifierGroupIds": [
        "STEP-MAIN",
        "STEP-SIDE",
        "STEP-DRINK"
      ]
    }
  ],
  "categories": [
    {
      "id": "cat-burgers",
      "name": "Burgers",
      "itemSkus": [
        "BURGER"
      ]
    },
    {
      "id": "cat-sides",
      "name": "Sides",
      "itemSkus": [
        "FRIES"
      ]
    },
    {
      "id": "cat-drinks",
      "name": "Drinks",
      "itemSkus": [
        "COLA"
      ]
    },
    {
      "id": "cat-menus",
      "name": "Menus",
      "itemSkus": [
        "MENU1"
      ]
    }
  ],
  "modifierGroups": [
    {
      "id": "G-EXTRAS",
      "name": "Extras",
      "minSelect": 0,
      "maxSelect": 3,
      "options": [
        {
          "sku": "X-CHEESE",
          "name": "Extra cheese",
          "priceMinor": 100
        },
        {
          "sku": "X-ONION",
          "name": "Onions",
          "priceMinor": 0
        }
      ]
    },
    {
      "id": "STEP-MAIN",
      "name": "Main",
      "minSelect": 1,
      "maxSelect": 1,
      "options": [
        {
          "sku": "BURGER",
          "name": "Cheeseburger",
          "priceMinor": 0,
          "isDefault": true
        }
      ]
    },
    {
      "id": "STEP-SIDE",
      "name": "Side",
      "minSelect": 1,
      "maxSelect": 1,
      "options": [
        {
          "sku": "FRIES",
          "name": "Fries",
          "priceMinor": 0,
          "isDefault": true
        }
      ]
    },
    {
      "id": "STEP-DRINK",
      "name": "Drink",
      "minSelect": 1,
      "maxSelect": 1,
      "options": [
        {
          "sku": "COLA",
          "name": "Cola 0.33 l",
          "priceMinor": 0,
          "isDefault": true,
          "depositMinor": 25
        }
      ]
    }
  ]
}
JSON200 OK
{
  "jobs": [
    {
      "connectionId": "7d0c2a4e-5b1f-4c8e-9a3d-2f6b8e1c4a70",
      "platform": "Mock",
      "platformStoreId": "mock-3f9a1c2e",
      "jobId": "a1f3c5e7-0b2d-4f6a-8c9e-1d3f5a7b9c0e",
      "status": "Completed",
      "externalJobId": null,
      "itemCount": 4,
      "lastError": null
    }
  ]
}
HTTPerrorMeaning
400empty_catalogproducts is empty.
400no_target_connectionsNo active connection to push to.
404unknown_connectionA connectionIds entry is not a connection of this tenant.

products[]

FieldTypeDescription
skurequiredstringYour POS id. Comes back in orders as items[].ref.sku. Unique within the menu.
namerequiredstringDisplay name.
priceMinorrequiredint64Gross price in minor units. 0 only for items whose price comes from modifiers.
availablebooleanDefault true. false = sold out.
taxRatePercentnumberVAT rate (7, 19 …). Most platforms require it.
descriptionstringDescription.
categoryNamestringCategory, when you do not send categories.
imageUrlstringPublic http(s) image URL.
barcodestringEAN / GTIN.
modifierGroupIdsstring[]Modifier groups offered with the item, in order.
depositMinorint64Deposit per unit, not included in priceMinor.
depositTaxRatePercentnumberVAT of the deposit; default = the item's rate.
allergensstring[]Allergen names in any language ("Gluten", "Milch", "Sellerie" …). RapidHub converts them to each platform's codes.
foodInfoobjectFood information, see below.

categories[] and modifierGroups[]

FieldTypeDescription
categories[].id / namestringCategory id and name.
categories[].itemSkusstring[]SKUs in display order. An item must be in a category to be visible.
modifierGroups[].id / namestringGroup id (referenced by modifierGroupIds) and the name the customer sees.
modifierGroups[].minSelect / maxSelectintSelection rules. 0/3 = up to three optional extras; 1/1 = exactly one (a combo step).
options[].sku / namestringOption SKU (comes back as modifiers[].sku) and name.
options[].priceMinorint64Surcharge (0 = included).
options[].availablebooleanDefault true.
options[].isDefaultbooleanPre-selected value.
options[].maxQuantityintHow often the option can be picked (default 1).
options[].taxRatePercent, depositMinor, allergens, imageUrl, foodInfo…As for products.

Combos, deposits, food info

  • Combo menus are a product whose modifierGroupIds are its steps; each step is a group with minSelect: 1, whose options use the product SKUs of the step's products (price = surcharge). In orders a combo arrives as one line whose modifiers are the chosen step products (see the combo example).
  • Deposits (Pfand) are separate from the price. In Germany Wolt accepts only the statutory amounts 0.25, 0.15 and 0.08 €; other amounts are not sent and the job reports a warning.
  • foodInfo carries EU food information (LMIV): additives (legal German phrases, e.g. "Mit Farbstoff(en)"), alcoholPercent, caffeineMgPer100ml, netAmount + netUnit (ml · g · pcs, used for the unit price), prepackaged, ingredients, producer, distributor, countryOfOrigin, storage, vegetarian, and nutrition per 100 g / 100 ml (per100ml, energyKj, energyKcal, fat, saturatedFat, carbohydrate, sugar, protein, salt).

Item updates

POST/pos/catalog/itemsX-Restaurant-Key

Mark items or modifier options sold out / available again, or change a price, by SKU, without sending the whole menu. Batch changes and send them together.

JSONRequest
{
  "items": [
    {
      "sku": "COLA",
      "available": false
    },
    {
      "sku": "BURGER",
      "available": true,
      "priceMinor": 950
    },
    {
      "sku": "X-CHEESE",
      "available": false
    }
  ]
}
JSON200 OK
{
  "jobs": [
    {
      "connectionId": "7d0c2a4e-5b1f-4c8e-9a3d-2f6b8e1c4a70",
      "platform": "Mock",
      "platformStoreId": "mock-3f9a1c2e",
      "jobId": "d2e4f6a8-1b3c-4d5e-9f7a-2b4c6d8e0f13",
      "status": "Completed",
      "externalJobId": null,
      "itemCount": 3,
      "lastError": null
    },
    {
      "connectionId": "1e5a9c3f-7b2d-4e8a-b6c0-4f9d2a7e1c38",
      "platform": "Wolt",
      "platformStoreId": "65f1c0a2b3d4e5f6a7b8c9d0",
      "jobId": "e3f5a7b9-2c4d-4e6f-8a0b-3c5d7e9f1a24",
      "status": "Queued",
      "externalJobId": null,
      "itemCount": 3,
      "lastError": "queued until 2026-10-15T12:45:00.0000000+00:00"
    }
  ]
}
  • available defaults to true; omit priceMinor to keep the price.
  • Rate-limited platforms queue. Wolt allows one item update per venue every 15 minutes. RapidHub merges your updates in a durable queue (the latest value per SKU wins) and sends them as soon as allowed. The job is then Queued with lastError: "queued until <time>". This is not an error.
  • A platform that rejects an update permanently (validation error) drops it from the queue; the job is Failed.
  • Errors: 400 no_items, 400 no_target_connections, 404 unknown_connection.

Push jobs

GET/pos/catalog/jobs/{jobId}X-Restaurant-Key

Every menu, item and store call creates a job. Asynchronous platforms finish later: Delivery Hero reports the result by callback, the legacy Glovo API is polled when you call this endpoint.

statusMeaning
PendingCreated, not sent yet.
QueuedWaiting: rate limit (item updates) or the platform's queue.
InProgressThe platform is processing it.
CompletedDone. lastError may still list warnings (… Warnings: a; b) or done_with_errors: …: show them to the user.
FailedRejected; lastError has the platform's message.

Wolt processes menus in the background without a status endpoint, so a Wolt menu job is Completed once Wolt accepted the upload. Wolt allows one menu upload per venue every 15 minutes; a second push within that time fails with "Wolt allows one menu upload per 15 minutes per venue."

Store status

GET/pos/catalog/{connectionId}/store-statusX-Restaurant-Key
JSON200 OK
{
  "isOpen": true,
  "isOnline": true,
  "openingTimes": [
    {
      "openDay": "MONDAY",
      "openTime": "11:00",
      "closeDay": "MONDAY",
      "closeTime": "22:00"
    },
    {
      "openDay": "FRIDAY",
      "openTime": "11:00",
      "closeDay": "SATURDAY",
      "closeTime": "02:00"
    },
    {
      "openDay": "SUNDAY",
      "openTime": "00:00",
      "closeDay": "SUNDAY",
      "closeTime": "24:00"
    }
  ]
}

isOpen / isOnline come from the platform (null = not reported). openingTimes is filled for Wolt and Mock. Unsupported platforms return 501 {"error":"not_supported"}; a failing platform call returns 502 {"error":"platform_error"}.

POST/pos/catalog/{connectionId}/store-statusX-Restaurant-Key
JSONPause until 18:00 UTC
{ "open": false, "closedUntil": "2026-10-15T18:00:00Z" }
JSON200 OK
{ "jobId": "c4d6e8f0-1a3b-4c5d-8e7f-9a0b1c2d3e4f", "status": "Completed", "lastError": null }
  • open: false pauses the store; closedUntil (ISO 8601) lets the platform reopen it automatically. Omit it for an indefinite pause; open: true reopens.
  • The response is 200 even if the platform refused: check status (Completed / Failed) and lastError.
  • Platform test stores often start offline, so their items look "sold out". Set them online once after the first menu push.

Opening hours

PUT/pos/catalog/{connectionId}/opening-hoursX-Restaurant-Key

Replaces the whole weekly schedule on the platform (Wolt, Mock). Send every window of the week as a JSON array. Success is 204 No Content.

  • Days: MONDAY … SUNDAY (case-insensitive). Times: HH:mm, 24-hour, in the store's local time. 24:00 is allowed as a closing time.
  • Overnight: close on the next day, e.g. FRIDAY 11:00 → SATURDAY 02:00.
  • Open 24/7: seven windows 00:00 → 24:00, one per day.
  • Several windows per day (lunch and dinner) are separate entries.
  • Errors: 400 invalid_day, 404, 501 not_supported, 502 platform_error.
JSONRequest
[
  {
    "openDay": "MONDAY",
    "openTime": "11:00",
    "closeDay": "MONDAY",
    "closeTime": "22:00"
  },
  {
    "openDay": "TUESDAY",
    "openTime": "11:00",
    "closeDay": "TUESDAY",
    "closeTime": "22:00"
  },
  {
    "openDay": "WEDNESDAY",
    "openTime": "11:00",
    "closeDay": "WEDNESDAY",
    "closeTime": "22:00"
  },
  {
    "openDay": "THURSDAY",
    "openTime": "11:00",
    "closeDay": "THURSDAY",
    "closeTime": "22:00"
  },
  {
    "openDay": "FRIDAY",
    "openTime": "11:00",
    "closeDay": "SATURDAY",
    "closeTime": "02:00"
  },
  {
    "openDay": "SATURDAY",
    "openTime": "12:00",
    "closeDay": "SUNDAY",
    "closeTime": "02:00"
  },
  {
    "openDay": "SUNDAY",
    "openTime": "12:00",
    "closeDay": "SUNDAY",
    "closeTime": "21:30"
  }
]
24/7 schedule
JSON
[
  { "openDay": "MONDAY",    "openTime": "00:00", "closeDay": "MONDAY",    "closeTime": "24:00" },
  { "openDay": "TUESDAY",   "openTime": "00:00", "closeDay": "TUESDAY",   "closeTime": "24:00" },
  { "openDay": "WEDNESDAY", "openTime": "00:00", "closeDay": "WEDNESDAY", "closeTime": "24:00" },
  { "openDay": "THURSDAY",  "openTime": "00:00", "closeDay": "THURSDAY",  "closeTime": "24:00" },
  { "openDay": "FRIDAY",    "openTime": "00:00", "closeDay": "FRIDAY",    "closeTime": "24:00" },
  { "openDay": "SATURDAY",  "openTime": "00:00", "closeDay": "SATURDAY",  "closeTime": "24:00" },
  { "openDay": "SUNDAY",    "openTime": "00:00", "closeDay": "SUNDAY",    "closeTime": "24:00" }
]

Sandbox (Mock platform)

The Mock platform is a delivery platform built into RapidHub. It behaves like a real platform toward your POS (same events, same endpoints, same responses) but has no connection to any real marketplace. As a second safety net, RapidHub never forwards an order marked platformExtras.mock = "1" to a real platform.

  1. Create a Mock connectionDashboard → Sandbox → Create Mock connection (or Restaurants → Manage → Connect a platform → Mock). It appears in GET /pos/catalog/connections with platform: "Mock" and every capability enabled.
  2. Push your menuThe Mock platform checks it like real platforms do: items without price, SKU, name, category or VAT rate; duplicate SKUs and empty menus (both fail the push); modifier groups with min > max or too few options; non-http(s) image URLs; unknown, empty or unused groups and categories. See the result under Sandbox → Catalog.
  3. Send test ordersSandbox → Send test order. Combine scenarios; products come from your pushed menu.
  4. Drive them from your POSEvery state call your POS sends is listed under Sandbox → Test orders with time, state, prep time, reject code, reason and HTTP status, plus a JSON download of the whole exchange.
  5. Simulate platform eventsPlatform cancel, courier picked up, delivered, accept deadline expired (auto-reject), customer changed the order.
ScenarioWhat the order contains
Delivery, platform courierchannel: Delivery, courierType: Platform, delivery fee 2.99
Delivery, own couriercourierType: Self; with cash, payment.cashToCollect
Pickupchannel: Pickup, no delivery fee
Paid online / cashpayment.status PaidOnline / CashOnDelivery (cash with a platform courier counts as paid)
Item with deposititems[].deposit (and modifiers[].deposit in combos)
Combo menuOne item whose modifiers are the menu steps
Modifiers incl. removalA paid extra and an Ohne … modifier priced 0
Item / basket discount, free deliveryitems[].discount and discounts[] with scope Item, Basket, Delivery
Tip, customer noteamounts.tip, customer.note and an item note
Scheduled (pre-order)timing: Scheduled, fulfillment.promisedFor
Unmapped itemA line with SKU UNMAPPED-n that is not in your menu
Quantity > 1The first line has quantity 2
Accepted by the platformacceptance: AutoByPlatform, arrives Accepted, no deadline

Mock orders have platform: "Mock", platformOrderId: "mock-<yyMMddHHmmss>-<n>", displayCode: "TEST-<n>", platformExtras.mock: "1" and an accept deadline (3 minutes by default). Item updates, store status and opening hours are stored per Mock connection and shown under Sandbox → Store; the Mock platform has no rate limits. Mock orders are not billed.

⚠ Fiscal warning

Test orders are only "test" if your POS treats them so. If a Mock order is paid on a fiscal POS, a real fiscal record is created: receipt, TSE / fiscal signature, Z report and any export (DSFinV-K, TicketBAI, VeriFactu …). Test on a training or test till, or cancel / void the receipt according to your country's rules. Detect test orders with platform == "Mock" or platformExtras.mock == "1" and, ideally, block fiscalization for them.

Postman

A ready-made collection covers every POS endpoint, with test scripts that carry ids from one request to the next, and a sandbox flow to run in order.

  1. In Postman: Import → paste https://hub.rapidpaygroup.com/integration/postman/collection.json, then the environment URL.
  2. Select the RapidHub environment and paste your restaurant key into restaurantKey.
  3. Run Auth check → Verify restaurant key, then the Sandbox flow folder step by step (step 4 waits for a test order you send from the dashboard).

Platform notes

RapidHub hides most platform differences. The ones that remain visible to a POS:

Wolt

  • Accept within 3 minutes (pre-orders: 15 minutes), otherwise Wolt rejects the order. If Wolt accepts orders itself for the venue, orders arrive with acceptance: "AutoByPlatform" and no deadline.
  • prepTimeMinutes becomes the new pickup time (at most +25 minutes over Wolt's estimate); for own-courier orders it sets the delivery time.
  • Self-delivery (courierType: "Self"): send Dispatched when your courier leaves and Delivered on arrival. With a Wolt courier, pickup and delivery are reported by Wolt.
  • Cash: with a Wolt courier the courier collects the cash, so the order is PaidOnline for you. With self-delivery it is CashOnDelivery with cashToCollect; platformExtras.cashGiven tells you what the customer will hand over.
  • No venue-cancel API after acceptance. Reject reasons are shown to the customer.
  • Menu: one full upload per venue every 15 minutes; item updates once per 15 minutes (queued). Wolt does not report VAT totals (amounts.tax is 0).

Uber Eats

  • Accept within about 11 minutes of RapidHub receiving the order (acceptDeadline).
  • Prep time and reject codes are not forwarded; a rejection is sent as "store unavailable".
  • Courier orders: pickup and delivery are tracked by Uber; your Dispatched / Delivered are recorded locally. Restaurant-delivered orders forward Dispatched (started) and Delivered as delivery status updates (best effort).

Delivery Hero (foodora, Glovo on the DH API …)

  • acceptDeadline comes from the platform per order. On accept RapidHub sends an acceptance time of at least now + 2 minutes: now + prepTimeMinutes, or the platform's own time when that is later; without a prep time the platform time, or now + 20 minutes.
  • If Delivery Hero is still dispatching when you accept, RapidHub retries the acceptance every 10 seconds for up to 5 minutes in the background; your call returns 200 immediately.
  • Cancelling a Received order is sent as a rejection. After acceptance there is no cancel API.
  • Ready is forwarded for platform-courier orders; for pickup and own delivery, Dispatched / Delivered send "picked up".
  • Prices arrive as decimal strings and are converted to minor units. Menu pushes are asynchronous; the job is updated by the platform's callback.

Just Eat Takeaway

  • One connector for the Just Eat Takeaway brands (Lieferando, Just Eat, Takeaway.com …); connections[].brand tells them apart.
  • No accept deadline is reported. Capabilities are currently all off.

Mock

  • Accepts every transition the state machine allows (an invalid one returns 409). See Sandbox.

Errors

Errors are JSON objects with a stable error code, plus context fields. Authentication failures and some "not found" cases have an empty body; rely on the HTTP status first.

HTTPerrorEndpointsMeaning
400invalid_state · invalid_reject_code · invalid_prep_timestateBody value not accepted.
400empty_catalog · no_items · no_target_connectionspush, itemsNothing to send / nowhere to send it.
400invalid_dayopening-hoursDay name not recognized.
401(empty)allMissing, unknown or inactive restaurant key.
403not_your_orderstateOrder of another restaurant.
404(empty) · unknown_connectionstate, catalog, storeUnknown order, job or connection.
409invalid_transition · state_mismatchstateSee state errors.
429(empty)allRate limit exceeded; retry after a minute.
501not_supportedstoreThe platform does not support it.
502failed · platform_errorstate, storeThe platform call failed; nothing changed. Retry with backoff.

Retries and idempotency. There are no idempotency keys. GET calls are safe to repeat. A repeated state call returns 409 invalid_transition once the first one succeeded: check the current state. Menu pushes and item updates are "last write wins" and safe to resend (mind the Wolt limits).

Rate limits

  • RapidHub: 600 requests per minute per client IP address across all endpoints. Exceeding it returns 429. Several tills behind one router share an IP, so prefer the real-time stream over frequent polling.
  • Platforms: Wolt allows one menu upload and one item update per venue every 15 minutes (RapidHub queues item updates for you). Other platforms apply their own limits; errors appear in the job's lastError.

Versioning

  • The REST paths are unversioned and kept backward compatible. The order schema has its own version in schemaVersion (currently 2.0).
  • 2.0 only added optional fields to 1.0: line VAT, deposits and discounts, discounts[], fees[], timing, acceptance, payment.cashToCollect, platformExtras, modifier SKUs. A 1.0 consumer keeps working.
  • Expect new optional fields, new enum values (states excepted) and new platforms at any time. Breaking changes would raise the major schemaVersion and be announced in advance.

Go-live checklist

  • The restaurant key is stored securely per till / location and can be replaced after a rotation.
  • The real-time connection reconnects with backoff, and every (re)connect is followed by GET /pos/orders/active.
  • Events are de-duplicated by orderId; the newest order replaces the stored one; states never move backwards.
  • The accept countdown uses acceptDeadline; AutoByPlatform orders show no accept button.
  • Canceled events and orders that vanish from the active list alert the staff.
  • Items map by ref.sku / modifiers[].sku; unknown SKUs are flagged, never silently dropped.
  • Receipts handle deposits, discounts (by fundedBy), fees, tips and cashToCollect.
  • 409 and 502 on state calls are handled; the staff are told to use the platform tablet when the platform is down.
  • UI decisions use capabilities, not platform names.
  • Mock / test orders cannot create real fiscal records.
  • Unknown JSON fields and enum values are ignored.