Server Side Cart

The Server-Side Cart is how orders are built and placed in PAR Ordering. Rather than assembling a basket in your client and sending a finished order, you keep the cart on our side: you tell us what the customer wants, and every response comes back with the cart fully priced — line totals, taxes, fees, discounts and tip all calculated for you. That means your client never has to reproduce our pricing rules, and what the customer sees is always what the POS will be sent.

The cart is also where order creation lives. There is no separate order-calculation call and no separate order-creation payload: you build the cart, check it out, initialize payment, and turn it into an order.

A cart moves through five calls, and the order matters:

  1. Initiate a cart to get a cart_id.
  2. Update it with the store, the order type and the customer's items. Repeat as often as you like — this is where pricing comes back.
  3. Check out to re-price the cart against the POS and confirm everything is still available and the chosen time still works.
  4. Initialize payment to get a payment hash.
  5. Create the order, passing that hash.

Checkout is the gate. A cart must be checked out before payment or order creation will be accepted, and any change to the cart afterwards means you need to check out again.

Core Features The API supports all essential cart operations including:

  • Cart creation and management
  • Item addition, removal, and quantity updates
  • Price calculation and tax handling
  • Discount code application
  • Inventory validation
  • Order conversion and checkout initialization

Initiate cart

Start here. The response gives you a cart_id, a long hexadecimal string that identifies the cart from then on — it is not a numeric ID or a UUID, so store it as an opaque string.

A guest can hold a cart without signing in. If the customer is already signed in, send their Authorization token and the cart is tied to them, which means only they can read it afterwards.

This endpoint is rate limited: 60 carts per minute for a signed-in customer, counted against their account, and 5 per minute for a guest, counted against the Device-UUID header, the caller's IP and your application together. Create one cart per customer session and reuse it rather than one per screen, or you will start receiving 429 responses — and always send Device-UUID, since guests without one share a single bucket.

Request

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/cart/init",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Device-UUID": "b6f0a5f2-2f1e-4a3c-9a1e-2c4c9f0d7f11",
    "Content-Type": "application/json"
  }
}

Send Content-Type: application/json even though there is no body. Without it the request is rejected with 415 and Only JSON content type allowed.

Response

{
  "status": "OK",
  "code": 200,
  "data": {
    "cart": {
      "id": "f6ce31f7b7549cc7c3e5326661cf1dc3b5ea5c3fd89cce634667e483e23fb51a",
      "is_empty": true,
      "metadata": {
        "cart_type": "",
        "rwg_token": null,
        "rwg_referral_brand_id": null
      }
    }
  }
}

Get cart

Reads the cart back, fully priced. The cart_id goes in the URL — it is a path segment, not a header.

If the cart belongs to a signed-in customer you must send that customer's Authorization token. Requesting someone else's cart returns 404 rather than 403, so treat a 404 here as either "no such cart" or "not this customer's cart".

Request

{
  "method": "get",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0"
  }
}

Response

{
  "status": "OK",
  "code": 200,
  "data": {
    "cart": {
      "id": "e6d0c777d7d46b1016ef80c8de20b132f53b2e74184bdc1e7c4bc3d840e5f07f",
      "products": [
        {
          "item_id": "14f4e650-987a-4000-b102-25629dfaa06e",
          "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
          "category_id": "f167fe8b-2f16-426f-a742-fe0c0624a60c",
          "price_id": "9a4e25be-9c32-4b69-81b7-19a4f3595406",
          "price": 530,
          "name": "Amer - Super Burger",
          "is_available": true,
          "comment": "",
          "quantity": 1,
          "tax_amount": 0,
          "tax_rate": 0,
          "product_groups": [
            {
              "id": "273ec6f0-4d3c-402b-955e-75c5ba581058",
              "type": 1,
              "comment": "",
              "products": [
                {
                  "item_id": "79737674-49fe-4053-a72b-9c6d5e0b8481",
                  "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
                  "category_id": "",
                  "price_id": "975ba87e-ac25-4da5-b04b-76844ef10421",
                  "price": 36,
                  "name": "Rye",
                  "is_available": true,
                  "comment": "",
                  "quantity": 1,
                  "tax_amount": 0,
                  "tax_rate": 0,
                  "product_groups": [],
                  "summary": {
                    "subtotal": 36,
                    "total": 36,
                    "total_tax": 0,
                    "tax_rates": []
                  }
                }
              ]
            }
          ],
          "summary": {
            "subtotal": 566,
            "total": 566,
            "total_tax": 0,
            "tax_rates": []
          }
        }
      ],
      "discounts": [],
      "tip": {
        "amount": 20,
        "type": 2
      },
      "fees": [
        {
          "amount": 100,
          "type": 4,
          "name": "Tech Fee"
        }
      ],
      "summary": {
        "subtotal": 566,
        "total": 679,
        "discount": 0,
        "payment_amount": 679,
        "tax_rates": [],
        "total_tax": 0,
        "total_fee": 0,
        "tip": 113,
        "loyalty": {
          "points_spent": 0,
          "points_accrued": 1,
          "available_points": 0
        }
      },
      "settings": {
        "prices_includes_tax": false
      }
    }
  }
}

Update cart

This is the call you will make most often. You send the cart as you want it to be — not a list of changes — so adding an item, changing a quantity, removing a line or applying a discount are all the same operation: send the new state of the cart.

The easiest way to work with it is to take the products array you got back from the previous call, modify it, and send it straight back. Of the product fields, we validate price_id, quantity, comment and product_groups; the rest are echoed back to you for convenience.

price_id is not the product id

This is the single most common cause of a rejected update. price_id identifies a price, not a product — a specific product at a specific place in a specific menu at a specific store. The product's own id is item_id.

The practical consequence: the same modifier has a different price_id depending on where it sits. Cheese under a burger's toppings group and cheese under a sandwich's toppings group are two different price_id values, even though they are the same product at the same price. So are the same product placed in two different categories.

Always take price_id from the exact node you are adding in the menu response — the modifier inside that product group, not the product looked up elsewhere. A price_id that is not a UUID is rejected with a 422; one that is a valid UUID but does not belong to this store comes back as Price UUID=`…` not found.

Treat it as stable only until the brand next deploys or syncs that menu. Prices surviving a POS price change keep their price_id, but a menu redistribution can replace them — so refresh from the menu API rather than replaying a cart payload you cached days ago.

Two things are required on every update and are easy to miss. metadata must always carry venue_id and order_type.id, even when neither has changed, and cart.tip must always be present — send {"amount": 0, "type": 1} if the customer has not left a tip. cart.discounts must be present too, as an empty array when there are none.

Anything specific to the order type goes inside metadata.order_type alongside id — the pickup or delivery time, the table for Dine-in, the vehicle for Curbside, the catering details for a catering order. Those fields are not checked here; they are validated when you check out, so a cart can hold an invalid pickup time right up until checkout rejects it.

Request

{
  "method": "put",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Content-Type": "application/json"
  },
  "body": {
    "metadata": {
      "venue_id": "a9d6d0c8-1689-4114-b1ec-6da9c33f0384",
      "order_type": {
        "id": 6,
        "pickup_asap": true
      }
    },
    "cart": {
      "products": [
        {
          "item_id": "14f4e650-987a-4000-b102-25629dfaa06e",
          "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
          "category_id": "f167fe8b-2f16-426f-a742-fe0c0624a60c",
          "price_id": "9a4e25be-9c32-4b69-81b7-19a4f3595406",
          "quantity": 1,
          "comment": "No onions please",
          "product_groups": [
            {
              "id": "273ec6f0-4d3c-402b-955e-75c5ba581058",
              "type": 1,
              "comment": "",
              "products": [
                {
                  "price_id": "975ba87e-ac25-4da5-b04b-76844ef10421",
                  "quantity": 1,
                  "comment": "",
                  "product_groups": []
                }
              ]
            }
          ]
        }
      ],
      "discounts": [],
      "tip": {
        "amount": 0,
        "type": 1
      }
    }
  }
}

Tip type is 1 for a fixed amount in minor units and 2 for a percentage.

Response

{
  "status": "OK",
  "code": 200,
  "data": {
    "cart": {
      "id": "e6d0c777d7d46b1016ef80c8de20b132f53b2e74184bdc1e7c4bc3d840e5f07f",
      "products": [
        {
          "item_id": "14f4e650-987a-4000-b102-25629dfaa06e",
          "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
          "category_id": "f167fe8b-2f16-426f-a742-fe0c0624a60c",
          "price_id": "9a4e25be-9c32-4b69-81b7-19a4f3595406",
          "price": 530,
          "name": "Amer - Super Burger",
          "is_available": true,
          "comment": "",
          "quantity": 1,
          "tax_amount": 0,
          "tax_rate": 0,
          "product_groups": [
            {
              "id": "273ec6f0-4d3c-402b-955e-75c5ba581058",
              "type": 1,
              "comment": "",
              "products": [
                {
                  "item_id": "79737674-49fe-4053-a72b-9c6d5e0b8481",
                  "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
                  "category_id": "",
                  "price_id": "975ba87e-ac25-4da5-b04b-76844ef10421",
                  "price": 36,
                  "name": "Rye",
                  "is_available": true,
                  "comment": "",
                  "quantity": 1,
                  "tax_amount": 0,
                  "tax_rate": 0,
                  "product_groups": [],
                  "summary": {
                    "subtotal": 36,
                    "total": 36,
                    "total_tax": 0,
                    "tax_rates": []
                  }
                }
              ]
            }
          ],
          "summary": {
            "subtotal": 566,
            "total": 566,
            "total_tax": 0,
            "tax_rates": []
          }
        }
      ],
      "discounts": [],
      "tip": {
        "amount": 20,
        "type": 2
      },
      "fees": [],
      "summary": {
        "subtotal": 566,
        "total": 679,
        "discount": 0,
        "payment_amount": 679,
        "tax_rates": [],
        "total_tax": 0,
        "total_fee": 0,
        "tip": 113,
        "loyalty": {
          "points_spent": 0,
          "points_accrued": 1,
          "available_points": 0
        }
      },
      "settings": {
        "prices_includes_tax": false
      }
    }
  }
}

Choosing a pickup or delivery time

Unless the order is ASAP, you need to offer the customer a time — and you should offer only times the store can actually honor, rather than a free-text field. Getting this right avoids the most common checkout failure.

Use Orders Filtered Pickup Times, for both pickup and delivery. It is the call that applies the store's preparation time, takes the cart into account, and drops slots that order throttling has already filled. The older Orders Pickup Times returns the store's raw serving windows and does none of that, so a time it offers can still be rejected at checkout.

The time-selection endpoint needs a singular point, even though the cart does not. The cart identifies the store with metadata.venue_id, but this call identifies it with a singular_point_id. Sending venue_id is rejected with `singular_point_id` parameter is required.

To find the right one, read the venue and look through its areas. Each area lists the order_types it serves, so pick the area whose list contains the order type you are ordering for and take its singular_point_id:

{
  "type_id": 4,
  "singular_point_id": "9254eb1c-ce27-4fa5-993e-c39096e7141d",
  "order_types": [{ "type_id": 6 }]
}

That is the Takeout area, so 9254eb1c-… is the singular point to use when asking for Takeout pickup times. See Areas for how areas map to order types.

Note the shape of the request. The order type is nested as order_type.id — a flat order_type_id is silently ignored, which is the mistake that makes this call appear to return the wrong slots. Pass cart_id once you have a cart: it is what makes the preparation time reflect what the customer is actually ordering.

Request

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/orders/filtered-pickup-times",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Content-Type": "application/json"
  },
  "body": {
    "singular_point_id": "9254eb1c-ce27-4fa5-993e-c39096e7141d",
    "order_type": {
      "id": 6,
      "properties": {}
    },
    "cart_id": "f6ce31f7b7549cc7c3e5326661cf1dc3b5ea5c3fd89cce634667e483e23fb51a"
  }
}

For delivery, send the delivery order type and put the customer's coordinates on order_type.delivery_info; you do not need a separate delivery-times call.

No customer token is required. A guest can ask for times with the application credentials alone.

in_advance_date is only for a later day. Omit it for today. Send it only when the store allows ordering days in advance — its days_in_advance is above zero — and only with a date after today and inside that window; anything else is rejected.

Three sets of fields from the old order-calculation call are not read here: menu_items and combo_meals, discounts and tip, and calculate_accurate_tax. The cart supplies all of that, so send cart_id instead of rebuilding the order in the request.

Response

{
  "status": "OK",
  "code": 200,
  "data": {
    "pickup_times": [
      "2026-09-19 10:05:00",
      "2026-09-19 10:10:00",
      "2026-09-19 10:15:00"
    ],
    "preparation_time": 12,
    "asap_time": 15,
    "is_fully_booked": false,
    "capacity_pause": null
  }
}

preparation_time and asap_time are what you show as "ready in about…". is_fully_booked being true with an empty list means the store has no capacity left for that day rather than being closed, and capacity_pause tells you when it resumes — worth distinguishing in the UI, because "fully booked until 2pm" is a very different message from "closed".

Put the chosen value on metadata.order_type.pickup_at (or delivery_at) and clear the ASAP flag. Fetch the times close to the moment the customer chooses rather than when they open the menu — slots fill up, and a stale list is the usual cause of a rejected checkout.

ASAP is not always available. If the store is closed, or outside its serving hours, pickup_asap: true will be rejected at checkout with code 2001 just like a stale explicit time would. Handle that case by falling back to the time list rather than assuming ASAP always works.

Checkout

Checkout is the moment of truth. We re-price the whole cart against the POS, confirm every item is still available, and check that the chosen pickup or delivery time is still valid. Only a cart that has been checked out can be paid for or turned into an order, and if you change the cart afterwards you have to check out again.

There is no request body — everything we need is already on the cart. You still have to send Content-Type: application/json, or the request is rejected with 415.

Expect this call to fail sometimes; it is where genuine problems surface, and handling its failures well is most of what makes an ordering flow feel solid. The three shapes below are the ones you will actually see.

The time is no longer available. Comes back as HTTP 400 with code 2001 for pickup or 2002 for delivery, and the requested time in info_message.title. This is the most common failure in practice, because a slot that was free when the customer started ordering may be full by the time they check out. Re-fetch the available times with Orders Filtered Pickup Times, ask the customer to choose again, update the cart and retry.

Something in the cart is no longer available. Also HTTP 400, but instead of info_message you get a products array of the offending product UUIDs. Mark those lines in the customer's basket, let them remove or replace the items, then update the cart and retry.

{
    "status": "Bad Request",
    "code": 400,
    "data": {
        "products": ["7618da71-a36d-42d0-a9ec-395631d456cf"],
        "context": null,
        "message": "There was an interruption and we couldn't do what you wanted us to do.",
        "error_id": ""
    }
}

The POS rejected the cart. HTTP 400 with a title naming the POS, for example Brink POS Exception with Invalid Items 649798645 in the body. This usually means an item is mapped incorrectly for that store rather than anything the customer did, so it is worth logging with the error_id and surfacing a generic message rather than asking them to retry.

Ordering can also be switched off for a venue entirely, which returns code 2003. See Error Codes for the full list.

Request

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}/checkout",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Content-Type": "application/json"
  }
}

Response

{
  "status": "OK",
  "code": 200,
  "data": {
    "cart": {
      "id": "e6d0c777d7d46b1016ef80c8de20b132f53b2e74184bdc1e7c4bc3d840e5f07f",
      "products": [
        {
          "item_id": "14f4e650-987a-4000-b102-25629dfaa06e",
          "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
          "category_id": "f167fe8b-2f16-426f-a742-fe0c0624a60c",
          "price_id": "9a4e25be-9c32-4b69-81b7-19a4f3595406",
          "price": 530,
          "name": "Amer - Super Burger",
          "is_available": true,
          "comment": "",
          "quantity": 1,
          "tax_amount": 0,
          "tax_rate": 0,
          "product_groups": [
            {
              "id": "273ec6f0-4d3c-402b-955e-75c5ba581058",
              "type": 1,
              "comment": "",
              "products": [
                {
                  "item_id": "79737674-49fe-4053-a72b-9c6d5e0b8481",
                  "menu_id": "5a4e114f-3c1f-4e59-af0f-7af915b3d495",
                  "category_id": "",
                  "price_id": "975ba87e-ac25-4da5-b04b-76844ef10421",
                  "price": 36,
                  "name": "Rye",
                  "is_available": true,
                  "comment": "",
                  "quantity": 1,
                  "tax_amount": 0,
                  "tax_rate": 0,
                  "product_groups": [],
                  "summary": {
                    "subtotal": 36,
                    "total": 36,
                    "total_tax": 0,
                    "tax_rates": []
                  }
                }
              ]
            }
          ],
          "summary": {
            "subtotal": 566,
            "total": 566,
            "total_tax": 0,
            "tax_rates": []
          }
        }
      ],
      "discounts": [],
      "tip": {
        "amount": 20,
        "type": 2
      },
      "fees": [
        {
          "amount": 100,
          "type": 4,
          "name": "Tech Fee"
        }
      ],
      "summary": {
        "subtotal": 566,
        "total": 679,
        "discount": 0,
        "payment_amount": 679,
        "tax_rates": [],
        "total_tax": 0,
        "total_fee": 0,
        "tip": 113,
        "loyalty": {
          "points_spent": 0,
          "points_accrued": 1,
          "available_points": 0
        }
      },
      "settings": {
        "prices_includes_tax": false
      }
    }
  }
}

Tip

As part of the checkout and cart update calls, you can optionally include a tip object to add a gratuity to the order.

      "tip": {
        "amount": 20,
        "type": 2
      }

The tip object contains the following fields:

  • amount: The value of the tip, which depends on the type.

  • type: Determines how the amount is interpreted:

    • 1 – A fixed amount in cents (e.g., amount: 200 means a $2.00 tip).

    • 2 – A percentage of the cart’s subtotal (e.g., amount: 15 applies a 15% tip based on the subtotal).

This allows tipping to be either a static fixed amount or dynamically calculated as a percentage of the order value.

Payment Initialization

Once the cart is checked out, tell us how the customer intends to pay. You get back a payment_init_hash which you pass to Order Create — that hash is how we tie the order to the payment.

Send the payment method the customer chose, either a payment_method_id from the store's available methods or a stored_payment_method_id for a card they have saved, together with the amount you expect to charge in minor units. The amount is required, and sending the cart total from the checkout response is the safest way to get it right.

Note that payment_method_id is the UUID of a method available at that store, not the small payment_method_type_id integer that describes the kind of payment. Read the UUIDs from Init application.

Alongside the hash, the response carries expires_in — the number of seconds the initialization stays valid, so do not sit on it — plus status_polling_interval as a suggested polling frequency, payment_processor_type_id, and an actions array with anything your client must do next, such as a redirect to a hosted card form. There is no id field; the hash is the identifier you carry forward.

Request

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}/payments/init",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Device-UUID": "b6f0a5f2-2f1e-4a3c-9a1e-2c4c9f0d7f11",
    "Content-Type": "application/json"
  },
  "body": {
    "payment_info": {
      "payment_method_id": "0d6b5a4e-3f23-11ed-936c-1a67b454859d",
      "amount": 566
    }
  }
}

Response

{
  "status": "string",
  "code": 0,
  "data": {
    "id": 0,
    "payment_method_id": 0,
    "payment_processor_type_id": 0,
    "payment_init_hash": "string",
    "allows_webhooks": true,
    "expires_in": 0,
    "status_polling_interval": 0,
    "additional_info": {}
  }
}

Order Create

The last step. This turns the checked-out, paid-for cart into a real order and sends it to the store.

Pass the payment_init_hash you received from Payment Initialization. If the customer is ordering as a guest, send their details in customer_info — this is how the store reaches them, and a missing or malformed phone number is rejected with code 2021 or 2022.

Card payments need a second value. When the customer pays by card through the hosted PAR Pay form, that form returns a one-time token, and you pass it alongside the hash as payment_info.one_time_token. Without it the processor has nothing to charge. Payment methods that do not involve the card form, such as cash, do not need it.

customer_info also takes two optional fields: optin_status_sms_internal, recording whether the customer agreed to SMS updates about their order, and demographics, for any demographic fields the brand has configured — read which ones are enabled from Init application.

This endpoint is idempotent: if you retry with the same X-Request-ID after a timeout, you will not create a second order. Always retry with the same request ID rather than a fresh one.

The order comes back nested under data.order, not at the root of data.

Request

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}/order",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Device-UUID": "b6f0a5f2-2f1e-4a3c-9a1e-2c4c9f0d7f11",
    "Content-Type": "application/json"
  },
  "body": {
    "customer_info": {
      "first_name": "Alex",
      "last_name": "Rivera",
      "email": "alex.rivera@example.com",
      "phone_number": "+13125550143"
    },
    "payment_info": {
      "payment_init_hash": "f6c81792c297a54e65c5f4a03204e7f3",
      "one_time_token": "30910032626134340597000708770932"
    }
  }
}

Once the order exists, follow it with GET Order details or, better, subscribe to order status webhooks so you are told when it changes rather than polling.

Response

{
  "status": "string",
  "code": 0,
  "data": {
    "id": 0,
    "code": "string",
    "uuid": "string",
    "type_id": 0,
    "reference_type": "string",
    "init_hash": "string",
    "external_reference": null,
    "external_code": null,
    "external_channel_id": null,
    "application_id": 0,
    "application_info": {
      "id": 0,
      "key": "string",
      "name": "string",
      "uuid": "string",
      "type_id": 0,
      "brand_id": 0,
      "created_at": "string",
      "updated_at": "string",
      "is_dpa_signed": 0,
      "reference_type": "string"
    },
    "venue_id": 0,
    "venue_info": {
      "id": 0,
      "name": "string",
      "address": "string",
      "city": "string",
      "zip": "string",
      "latitude": 0,
      "longitude": 0,
      "phone": "string",
      "delivery_travel_type": "string",
      "use_pos_order_number": true,
      "timezone": {
        "name": "string",
        "offset": "string"
      },
      "tax_number": "string",
      "pickup_time": 0,
      "image": null,
      "country": {
        "id": 0,
        "name": "string",
        "code": "string",
        "calling_code": "string",
        "currency_settings": {
          "currency_space": true,
          "decimal_separator": "string",
          "thousands_separator": "string",
          "symbol_position": "string"
        }
      },
      "currency": {
        "id": 0,
        "code": "string",
        "code_numeric": "string",
        "symbol": "string",
        "rounding_unit": 0,
        "rounding_unit_tip": 0
      },
      "dispatch_legacy": true
    },
    "singular_point_id": 0,
    "singular_point_info": {
      "id": 0,
      "input": {
        "id": 0,
        "name": "string",
        "singular_point_id": 0
      },
      "input_id": 0,
      "area_info": {
        "id": 0,
        "name": "string",
        "pos_id": null,
        "type_id": 0,
        "reference_type": "string"
      },
      "parent_id": 0,
      "created_at": null,
      "input_type": "string",
      "table_info": {
        "id": 0,
        "number": "string",
        "pos_id": null
      },
      "parent_type": "string"
    },
    "pos_ticket_id": "string",
    "pos_ticket_code": "string",
    "customer_account_id": 0,
    "customer_account_info": {
      "id": 0,
      "uuid": "string",
      "email": "string",
      "state": 0,
      "locale": "string",
      "type_id": 0,
      "brand_id": 0,
      "v2_token": null,
      "confirmed": 0,
      "last_name": "string",
      "created_at": "string",
      "first_name": "string",
      "updated_at": "string",
      "customer_id": null,
      "demographics": [
        {}
      ],
      "phone_number": "string",
      "is_dpa_signed": 0,
      "discount_cards": [
        {}
      ],
      "reference_type": "string",
      "optin_status_pn": 0,
      "optin_status_sms": 0,
      "optin_status_email": 0,
      "created_by_application_id": 0,
      "optin_status_sms_internal": 0
    },
    "order_type": 0,
    "order_type_info": {
      "id": 0,
      "uuid": "string",
      "state": 0,
      "tip_max": null,
      "type_id": 0,
      "venue_id": 0,
      "created_at": "string",
      "updated_at": "string",
      "tip_default": null,
      "is_dpa_signed": 0,
      "reference_type": "string",
      "external_channel_only": 0
    },
    "order_type_properties": {
      "type_id": 0,
      "table_id": "string"
    },
    "order_additional_info": {
      "pos_wait_until_paid": true
    },
    "is_scheduled": true,
    "trigger_type": 0,
    "status": "string",
    "status_changed_by": "string",
    "payment_status": "string",
    "fraud_detection_status": null,
    "fiscal_status": null,
    "fiscalization_number": null,
    "tablet_status": "string",
    "delivery_status": null,
    "is_test": true,
    "calculation_method": "string",
    "subtotal": 0,
    "discount": 0,
    "subsidy": 0,
    "external_payment_amount": 0,
    "discount_info": null,
    "service_charge": 0,
    "service_charge_rate": 0,
    "service_charge_tax_rate": 0,
    "delivery_fee_tax_rate": 0,
    "delivery_fee": 0,
    "delivery_fee_tax": 0,
    "minimal_order_amount": 0,
    "minimum_surcharge": 0,
    "minimum_surcharge_tax": 0,
    "tip": 0,
    "tip_rate": 0,
    "fees": [
      {
        "name": "Tech Fee",
        "type": 4,
        "total": 150,
        "subtotal": 130,
        "tax": 20
      }
    ],
    "total": 0,
    "tax": 0,
    "tax_exemption": 0,
    "tax_rates": [
      {}
    ],
    "tax_rates_exemption": [
      {}
    ],
    "fees": [
      {
        "name": "Tech Fee",
        "type": 4,
        "total": 150,
        "subtotal": 130,
        "tax": 20
      }
    ],
    "refunded": 0,
    "products": [
      {
        "pos_id": null,
        "name": "string",
        "translations": {
          "name": null
        },
        "price": 0,
        "quantity": 0,
        "subtotal": 0,
        "tax": 0,
        "tax_rates": [
          {}
        ],
        "comment": "string",
        "image": {
          "thumbnail_small": "string",
          "thumbnail_medium": "string",
          "fullsize": "string"
        },
        "product_groups": [
          {
            "group_type": 0,
            "pos_id": null,
            "name": "string",
            "translations": {
              "name": null
            },
            "subtotal": 0,
            "tax": 0,
            "tax_rates": [
              {}
            ],
            "image": {
              "thumbnail_small": "string",
              "thumbnail_medium": "string",
              "fullsize": "string"
            },
            "products": [
              {
                "pos_id": null,
                "name": "string",
                "translations": {
                  "name": null
                },
                "price": 0,
                "quantity": 0,
                "subtotal": 0,
                "tax": 0,
                "tax_rates": [
                  {}
                ],
                "comment": "string",
                "image": {
                  "thumbnail_small": "string",
                  "thumbnail_medium": "string",
                  "fullsize": "string"
                }
              }
            ]
          }
        ]
      }
    ],
    "payments": [
      {
        "id": 0,
        "payment_method_id": 0,
        "payment_processor_type_id": 0,
        "payment_processor_id": null,
        "total": 0,
        "pos_receipt_id": "string",
        "stored_payment_method_vault": null
      }
    ],
    "refunds": [
      {}
    ],
    "pickup_code": null,
    "preparation_time": 0,
    "additional_delivery_info": null,
    "delivery_delay_time": null,
    "is_customer_arrived": true,
    "is_status_fired": true,
    "cancellation_reason_id": null,
    "delivery_job": {},
    "regret_cancel_until": null,
    "send_at": "string",
    "pickup_at": null,
    "ready_at": null,
    "arriving_at": null,
    "delivery_at": null,
    "created_at": "string",
    "updated_at": "string"
  }
}

Other cart operations

Two more operations are worth knowing about, because the alternative is usually to throw the cart away and start again.

Empty the cart

POST /api/cart/{cart_id}/reset clears the cart's contents while keeping the cart itself. Use it when the customer wants to start their basket over, rather than initializing a new cart — that keeps you within the rate limit on cart creation.

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}/reset",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Content-Type": "application/json"
  }
}

Move the cart to another store

POST /api/cart/{cart_id}/transfer-venue moves an existing cart to a different store. This is what you want when the customer's chosen store turns out to be closed, cannot deliver to their address, or they simply pick somewhere else — rather than making them rebuild their basket.

venue_id is required. You can also pass order_type if it is changing at the same time, including order_type.properties.table_id when moving to a Dine-in (FS) table.

Every price_id in the cart changes. Prices belong to a store, so the response comes back with a new price_id on every line. Use the products from this response for your next update or checkout — replaying the cart payload you held before the transfer will fail, because those price ids do not exist at the new store.

Items the new store does not carry are dropped, so compare the returned cart with what the customer had and tell them what is missing.

Items that do not exist on the new store's menu cannot come across, so re-read the cart afterwards and tell the customer what changed before they check out.

{
  "method": "post",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}/transfer-venue",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Content-Type": "application/json"
  },
  "body": {
    "venue_id": "b7c4e1a2-9f3d-4c58-8e21-5d0a7b6c3f84"
  }
}

After the order is placed

Two operations act on the order rather than the cart.

Let the customer cancel

POST /api/orders/{order_uuid}/cancel cancels an order the customer has just placed. Whether this is allowed at all is a brand setting, and in some cases a store setting — see the customer-cancellation table in Order Statuses. There is a time window, and once it closes the call is rejected with code 2013, so hide or disable your cancel button once that window has passed rather than letting the customer discover it the hard way.

Poll for status

POST /api/orders/{order_uuid}/poll is the lightweight way to ask whether anything has changed, and it returns 410 Gone once the order has reached a state where there is nothing left to poll for. Prefer order status webhooks where you can — they tell you the moment something changes instead of you asking repeatedly — and keep polling as the fallback for clients that cannot receive webhooks.

Cross-Sell API

The cross-sell API provides product recommendations to the user. This feature introduces a new property to both the cart GET and PUT API calls to manage the source of these recommendations.

GET Cross-Sell Products

This endpoint retrieves a list of recommended products based on the current cart contents. It returns a new top-level property, cross_sell_type, which indicates the source of the recommendations.

Request

{
  "method": "get",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}/cross-sell",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0"
  }
}

Response

{
  "status": "OK",
  "code": 200,
  "data": {
    "cart": {
      "id": "e6d0c777d7d46b1016ef80c8de20b132f53b2e74184bdc1e7c4bc3d840e5f07f",
      "cross_sell_type": {
        "label": "menu",
        "value": 0
      },
      "cross_sell_products": [
        {
          "price_id": "cabf785e-32d6-4358-98ed-8eb401025c35",
          "name": "Recommended Drink",
          "price": 300,
          "is_available": true
        }
      ]
    }
  }
}

The cross_sell_type property can have one of two forms:

  • { label: "menu", value: 0 }: Recommendations are sourced from the menu system.
  • { label: "punchh", value: 1 }: Recommendations are sourced from Punchh.

Update Cart with Cross-Sell Product

When adding a cross-sell product to the cart, the cross_sell_type property is added to the product object. This property should only contain the value from the GET /api/cart/{cart_id}/cross-sell response.

Request

{
  "method": "put",
  "url": "https://api-public-demo.menu.app/api/cart/{cart_id}",
  "headers": {
    "X-Request-ID": "69da3547-204b-4093-a225-54e084c24215",
    "Application": "f3a90488ffee32c3acb6fcd0ca417cf6",
    "Api-Version": "4.78.0",
    "Content-Type": "application/json"
  },
  "body": {
    "metadata": {},
    "customer_info": {},
    "cart": {
      "products": [
        {
          "price_id": "dc7b5bf2-4d1f-471a-995e-be7cf7d8a214",
          "comment": "",
          "quantity": 50,
          "product_groups": []
        },
        {
          "price_id": "cabf785e-32d6-4358-98ed-8eb401025c35",
          "comment": "",
          "quantity": 5,
          "product_groups": [],
          "cross_sell_type": 1
        }
      ],
      "discounts": [],
      "tip": {}
    }
  }
}